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")funcrun()error{const srcFileName ="subdomains.txt"// Host de destino - argumento de inicializaçãoiflen(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 redirecionamentosreturn 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 interessacontinue} 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)}returnnil}funcmain(){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.
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:
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.