Go Fuzzing! Escrevendo seu próprio scanner de subdomínios para aprender paralelismo em Golang

Go Fuzzing! Escrevendo seu próprio scanner de subdomínios para aprender paralelismo em Golang

Aprenda a usar goroutines em Go para criar um scanner de subdomínios que utiliza paralelismo. O artigo aborda conceitos como http.Client, sync.WaitGroup e sync.Mutex, além de fornecer um código prático para a enumeração de subdomínios.

MundiX News·23 de maio de 2026·10 min de leitura·👁 8 views

A execução de tarefas como enumeração de subdomínios, força bruta de senhas ou hash cracking se beneficia do paralelismo. A linguagem Go é uma excelente escolha para isso. Hoje, vamos dominar o conceito de goroutines – threads leves gerenciadas por um agendador próprio no espaço do usuário, em vez do sistema operacional, o que muda radicalmente a carga no sistema. Ao mesmo tempo, vamos escrever nosso próprio fuzzing de subdomínios.

Neste e nos artigos subsequentes, analisaremos as principais técnicas para trabalhar com goroutines, ilustrando-as com tarefas simples de enumeração de valores. Não vamos nos aprofundar na metodologia de enumeração – fuzzing, hash cracking e assim por diante. Afinal, estamos aprendendo a codificar e, portanto, agora nos concentraremos nas ferramentas fornecidas pela linguagem de programação.

Teoria e termos serão um pouco mais presentes do que nos artigos anteriores, porque o tema em si é muito amplo e, francamente, não é o mais simples. Tentei apresentar o material de forma que, na primeira leitura, você possa pular o que é completamente chato, mas para uma melhor compreensão, ainda recomendo que você se aprofunde!

Então, vamos escrever uma utilidade simples para encontrar subdomínios (enumeração de subdomínios) da maneira mais óbvia – por força bruta através de uma lista.

A lista não é difícil de encontrar na internet, por exemplo, em SecLists ou n0kovo_subdomains, mas para fins de aprendizado, para não bombardear um host inocente com solicitações (especialmente porque ainda não aprendemos a parar goroutines ou controlar seu número), aconselho você a escrever a sua própria, literalmente a partir de uma dúzia de posições – isso é mais do que suficiente para começar.

Mais detalhes sobre a pesquisa de subdomínios podem ser encontrados no artigo "Web Fuzzing do início. Aprendendo a enumerar diretórios e encontrar arquivos ocultos em sites".

Vamos criar um novo módulo:

mkdir sudbdomain && cd $_ go mod init sudbdomain touch main.go

No arquivo main.go, inseriremos o seguinte código:

go
package main

import (
	"bufio"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"sync"
	"time"
)

func run() error {
	const srcFileName = "subdomains.txt"
	// Host de destino - argumento de inicialização
	if len(os.Args) <= 1 {
		fmt.Fprintf(os.Stderr, "Target address not specified\n")
		os.Exit(1)
	}
	host := os.Args[1]
	// Abrimos a lista de subdomínios
	srcFile, err := os.Open(srcFileName)
	if err != nil {
		return fmt.Errorf("opening %s: %w", srcFileName, err)
	}
	defer srcFile.Close()
	// Configurando o cliente HTTP
	client := &http.Client{
		Timeout: 1 * time.Second,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			// Ignorando redirecionamentos
			return http.ErrUseLastResponse
		},
	}
	// Lemos e verificamos os subdomínios linha por linha
	scanner := bufio.NewScanner(srcFile)
	for scanner.Scan() {
		sub := strings.TrimSpace(scanner.Text())
		if sub == "" {
			continue
		}
		target := "https://" + sub + "." + host
		resp, err := client.Get(target)
		if err != nil {
			continue
		}
		resp.Body.Close()
		io.ReadAll(resp.Body)
		if resp.StatusCode == http.StatusNotFound {
			// 404 não nos interessa
			continue
		}
		fmt.Printf("%s - %d %s\n", target, resp.StatusCode, http.StatusText(resp.StatusCode))
	}
	if err := scanner.Err(); err != nil {
		return fmt.Errorf("reading %s: %w", srcFileName, err)
	}
	return nil
}

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

O host de destino, para o qual procuraremos subdomínios, é definido como um argumento de inicialização: seu valor é lido como o segundo elemento da matriz os.Args (o primeiro elemento com o índice 0 contém o nome do arquivo executável). A lista de subdomínios para consulta é lida do arquivo. Para simplificar, o nome do arquivo é definido como uma constante srcFileName, mas, é claro, ele também pode ser passado como um argumento, através de uma variável de ambiente ou definido em uma configuração.

Em seguida, usamos a leitura em buffer por meio do pacote bufio: lemos o arquivo com a lista linha por linha e, para cada linha, iniciamos uma verificação de acessibilidade do host. Finalmente, usamos o padrão main-run para facilitar o tratamento de erros.

Quando ocorre um erro, a saída antecipada da função run() é executada com o retorno desse erro. Na função main(), é verificado se ocorreu um erro e, nesse caso, o código 1 é retornado. Ao mesmo tempo, as chamadas defer são tratadas corretamente, o que não teria acontecido se os.Exit(1) fosse chamado imediatamente.

É conveniente trabalhar com configurações usando o pacote Viper. Ele suporta JSON, YAML, INI e muitas outras opções. Como alternativa, você pode considerar koanf e cleanenv com capacidades semelhantes. Além disso, GoDotEnv é frequentemente usado, o que permite carregar variáveis de ambiente definidas no arquivo .env.

Para interação de rede, usaremos http.Client do pacote da biblioteca padrão net/http, que permite enviar solicitações de rede e receber respostas. Observe: criamos uma instância configurada da estrutura http.Client com antecedência e, no futuro, trabalhamos com um ponteiro para ela – este é o símbolo & na definição da variável client. Não queremos copiar o cliente novamente para cada nova solicitação e intencionalmente usamos um ponteiro para reutilizar a mesma instância e, assim, usar os recursos de forma mais eficiente.

http.Client é thread-safe, portanto, usamos a mesma instância através desse ponteiro, mesmo em diferentes goroutines.

O restante do artigo está disponível apenas para membros.

📤 Compartilhar & Baixar