O teste de penetração permite que as organizações segmentem em possíveis fraquezas de segurança em uma rede e forneçam a necessidade de corrigir vulnerabilidades antes de serem comprometidas por um ator malicioso.
Neste artigo, criaremos um scanner de vulnerabilidade de rede simples e razoavelmente robusto usando o GO, um idioma que é muito adequado para programação de rede, pois foi projetado com a concorrência em mente e com uma ótima biblioteca padrão.
1. Configurando nosso projeto
Crie um scanner de vulnerabilidade
Queremos criar uma ferramenta cli simples que pudesse digitalizar uma rede de hosts, encontrar portas abertas, executar serviços e descobrir possível vulnerabilidade. O scanner vai ser muito simples de começar, mas crescerá cada vez mais capaz à medida que colocamos os recursos.
Então, primeiro, criaremos um novo projeto Go:
mkdir goscan
cd goscan
go mod init github.com/yourusername/goscan
Isso inicializa um novo módulo GO para o nosso projeto, que nos ajudará a gerenciar dependências.
Configurando pacotes e ambiente
Para o nosso scanner, alavancaremos vários pacotes Go:
package main
import (
"fmt"
"net"
"os"
"strconv"
"sync"
"time"
)
func main() {
fmt.Println("GoScan - Network Vulnerability Scanner")
}
Esta é apenas a nossa configuração inicial. Isso será suficiente para alguns recursos iniciais, mas adicionaremos mais importações sob demanda. Agora, outros pacotes de bibliotecas padrão, como a NET, terão o cuidado de fazer a maior parte das redes que precisamos e a sincronização fará simultaneidade, etc.
Considerações éticas e riscos com varredura de rede
Agora, antes de entrarmos na implementação, devemos abordar algumas considerações éticas em torno da digitalização da rede. A varredura ou enumeração não autorizada de rede é ilegal em muitas partes do mundo e é tratada como um vetor para um ataque cibernético. Você sempre deve seguir estas regras:
- Permissão: Somente digitalize redes e sistemas que você possui ou possui permissão explícita para digitalizar.
- Escopo: Defina um escopo claro para sua digitalização e não o exceda.
- Tempo: Não escolha hiper-scan que possa reduzir os serviços ou aumentar os alertas de segurança.
- Divulgação: Se você descobrir vulnerabilidades, faça -as de maneira responsável, relatando -as aos proprietários apropriados do sistema.
- Conformidade legal: Entenda e cumpra as leis locais que regem a digitalização da rede.
O uso indevido de ferramentas de varredura pode resultar em ação legal, danos ao sistema ou negação acidental de serviço. Nosso scanner incluirá salvaguardas como a limitação de taxas, mas a responsabilidade está finalmente com o usuário para empregá -lo eticamente.
2. Scanner de porta simples
A avaliação de vulnerabilidade é baseada na varredura de portas. Os potenciais serviços vulneráveis que estão sendo oferecidos em cada uma dessas portas abertas são as informações que estamos procurando. Agora, vamos escrever um scanner de porta simples em Go.
Implementação de baixo nível da digitalização portuária
Digitalização da porta: tente estabelecer uma conexão com todas as portas possíveis em um host de destino. Se a conexão for bem -sucedida, a porta estará aberta; Se falhar, a porta será fechada ou filtrada. Para essa funcionalidade, o pacote de rede do Go nos abordou.
Então, aqui está a nossa versão de um simples scanner de porta:
package main
import (
"fmt"
"net"
"time"
)
func scanPort(host string, port int, timeout time.Duration) bool {
target := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
return false
}
conn.Close()
return true
}
func main() {
host := "localhost"
timeout := time.Second * 2
fmt.Printf("Scanning host: %s\n", host)
for port := 1; port <= 1024; port++ {
if scanPort(host, port, timeout) {
fmt.Printf("Port %d is open\n", port)
}
}
fmt.Println("Scan complete")
}
Usando o pacote de rede
O código acima utiliza o pacote Go Net, que fornece interfaces e funções de E/S da rede. Então, quais são as peças principais?
- net.dialtimeout: Esta função tenta se conectar ao endereço de rede TCP com um tempo limite. Ele retorna uma conexão e um erro, se houver.
- Manuseio de conexão: Se ele se conectar sem problemas, sabemos que está aberto e fechamos a conexão imediatamente para abrir recursos.
- Parâmetro de tempo limite: Especificamos um tempo limite para evitar pendurar em quaisquer portas abertas filtradas. Dois segundos é um bom valor inicial, mas isso pode ser ajustado de acordo com as condições da rede.
Testando nossa primeira varredura
Agora, vamos executar nosso scanner simples contra o nosso localhost, onde podemos ter alguns serviços em execução.
- Salve o código em um arquivo nomeado
main.go
- Execute -o com
go run main.go
Isso mostrará quais portas locais estão abertas. Em uma máquina de desenvolvimento normal, você pode ter 80 (http), 443 (https) ou qualquer número de portas de banco de dados em uso com base em quais serviços você possui.
Aqui está alguma saída de amostra que você pode obter:
Scanning host: localhost
Port 22 is open
Port 80 is open
Port 443 is open
Scan complete
Usando esse scanner básico funciona, mas vem com algumas grandes desvantagens:
- Velocidade: É dolorosamente lento, pois digitaliza as portas sequencialmente.
- Informação: Apenas nos diz se uma porta está aberta, sem informações de serviço.
- Faixa limitada: Nós apenas vamos digitalizar as primeiras 1024 portas.
Essas restrições tornam impraticável nosso scanner no mundo real.
3. Melhorando a partir daqui: varredura multi-thread
Por que a primeira versão é lenta
Nosso primeiro scanner de porta funciona, embora seja dolorosamente lento para ser utilizável. O problema é o seu método seqüencial – sondando uma porta de cada vez. Quando um host possui muitas portas fechadas/filtradas, perdemos tempo aguardando uma conexão com o tempo em cada porta antes de mudarmos para a outra.
Para mostrar o problema, vamos dar uma olhada no momento do nosso scanner básico:
- O pior caso de digitalizar as primeiras 1024 portas levaria no máximo 2048 segundos (mais de 34 minutos) com 2 segundos no tempo limite
- Mas mesmo quando as conexões com as portas fechadas falham imediatamente, esse método é ineficiente devido à latência da rede.
Essa abordagem um por um é um gargalo para qualquer ferramenta real de varredura de vulnerabilidades.
Adicionando suporte de encadeamento
O GO é particularmente bom em simultaneidade usando goroutines e canais. Portanto, aproveitamos esses recursos para tentar digitalizar várias portas de uma só vez, o que aumenta significativamente o desempenho.
Agora, vamos criar um scanner de porta multithread:
package main
import (
"fmt"
"net"
"sync"
"time"
)
type Result struct {
Port int
State bool
}
func scanPort(host string, port int, timeout time.Duration) Result {
target := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
return Result{Port: port, State: false}
}
conn.Close()
return Result{Port: port, State: true}
}
func scanPorts(host string, start, end int, timeout time.Duration) ()Result {
var results ()Result
var wg sync.WaitGroup
resultChan := make(chan Result, end-start+1)
semaphore := make(chan struct{}, 100)
for port := start; port <= end; port++ {
wg.Add(1)
go func(p int) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
result := scanPort(host, p, timeout)
resultChan <- result
}(port)
}
go func() {
wg.Wait()
close(resultChan)
}()
for result := range resultChan {
if result.State {
results = append(results, result)
}
}
return results
}
func main() {
host := "localhost"
startPort := 1
endPort := 1024
timeout := time.Millisecond * 500
fmt.Printf("Scanning %s from port %d to %d\n", host, startPort, endPort)
startTime := time.Now()
results := scanPorts(host, startPort, endPort, timeout)
elapsed := time.Since(startTime)
fmt.Printf("\nScan completed in %s\n", elapsed)
fmt.Printf("Found %d open ports:\n", len(results))
for _, result := range results {
fmt.Printf("Port %d is open\n", result.Port)
}
}
Resultados de vários tópicos
Agora, vamos dar uma olhada nos ganhos de desempenho e nos mecanismos de simultaneidade que adicionamos ao nosso scanner aprimorado:
- Goroutines: Para tornar a digitalização eficiente, iniciamos uma goroutina para cada porta que precisamos digitalizar, portanto, enquanto verificamos uma porta, podemos verificar outras portas simultaneamente.
- Grupo de espera: A sincronização. Grupos de espera induzimos goroutines, queremos esperar pela conclusão deles. O WaitGroup nos ajuda a rastrear todas as goroutinas em execução e a aguardar a conclusão.
- Canal de resultado: Criamos um canal de buffers para obter resultados de todas as goroutinas em ordem.
- Padrão semáforo: Um semáforo é usado, implementado usando um canal, que limita o número de varreduras permitidas em paralelo. É o que nos impede de sobrecarregar o sistema de destino real ou mesmo nossa própria máquina com tantas conexões abertas.
- Tempo limite reduzido: Como executamos muitas dessas varreduras de maneira paralela, usamos um tempo limite mais baixo.
A lacuna de desempenho é substancial. Portanto, quando implementamos isso, ele pode nos deixar digitalizar 1024 portas em minutos e certamente menos de meia hora.
Saída de amostra:
Scanning localhost from port 1 to 1024
Scan completed in 3.2s
Found 3 open ports:
Port 22 is open
Port 80 is open
Port 443 is open
A abordagem multithread escala muito bem para intervalos de porta maiores e vários hosts. O padrão de semáforo garante que não fiquemos sem recursos do sistema, apesar da digitalização de mais de mil portos.
4. Adicionando detecção de serviço
Agora que temos um scanner de porta rápido e eficiente, a próxima etapa é saber quais serviços estão sendo executados nessas portas abertas. Isso é comumente conhecido como “impressão digital de serviço” ou “agarrar banner”, um processo pelo qual conectamos para abrir portas e examinar os dados retornados.
Implementação de Banner pegando
A pegada de banner é quando abrimos um serviço e lemos a resposta (Banner) que ela nos envia. Portanto, é uma boa maneira de identificar se algo funciona, pois muitos serviços se identificam nessas banners.
Agora vamos adicionar banner pegando ao nosso scanner:
package main
import (
"bufio"
"fmt"
"net"
"strings"
"sync"
"time"
)
type ScanResult struct {
Port int
State bool
Service string
Banner string
Version string
}
func grabBanner(host string, port int, timeout time.Duration) (string, error) {
target := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
return "", err
}
defer conn.Close()
conn.SetReadDeadline(time.Now().Add(timeout))
if port == 80 || port == 443 || port == 8080 || port == 8443 {
fmt.Fprintf(conn, "HEAD / HTTP/1.0\r\n\r\n")
} else {
}
reader := bufio.NewReader(conn)
banner, err := reader.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(banner), nil
}
func identifyService(port int, banner string) (string, string) {
commonPorts := map(int)string{
21: "FTP",
22: "SSH",
23: "Telnet",
25: "SMTP",
53: "DNS",
80: "HTTP",
110: "POP3",
143: "IMAP",
443: "HTTPS",
3306: "MySQL",
5432: "PostgreSQL",
6379: "Redis",
8080: "HTTP-Proxy",
27017: "MongoDB",
}
service := "Unknown"
if s, exists := commonPorts(port); exists {
service = s
}
version := "Unknown"
lowerBanner := strings.ToLower(banner)
if strings.Contains(lowerBanner, "ssh") {
service = "SSH"
parts := strings.Split(banner, " ")
if len(parts) >= 2 {
version = parts(1)
}
}
if strings.Contains(lowerBanner, "http") || strings.Contains(lowerBanner, "apache") ||
strings.Contains(lowerBanner, "nginx") {
if port == 443 {
service = "HTTPS"
} else {
service = "HTTP"
}
if strings.Contains(banner, "Server:") {
parts := strings.Split(banner, "Server:")
if len(parts) >= 2 {
version = strings.TrimSpace(parts(1))
}
}
}
return service, version
}
func scanPort(host string, port int, timeout time.Duration) ScanResult {
target := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
return ScanResult{Port: port, State: false}
}
conn.Close()
banner, err := grabBanner(host, port, timeout)
service := "Unknown"
version := "Unknown"
if err == nil && banner != "" {
service, version = identifyService(port, banner)
}
return ScanResult{
Port: port,
State: true,
Service: service,
Banner: banner,
Version: version,
}
}
func scanPorts(host string, start, end int, timeout time.Duration) ()ScanResult {
var results ()ScanResult
var wg sync.WaitGroup
resultChan := make(chan ScanResult, end-start+1)
semaphore := make(chan struct{}, 100)
for port := start; port <= end; port++ {
wg.Add(1)
go func(p int) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
result := scanPort(host, p, timeout)
resultChan <- result
}(port)
}
go func() {
wg.Wait()
close(resultChan)
}()
for result := range resultChan {
if result.State {
results = append(results, result)
}
}
return results
}
func main() {
host := "localhost"
startPort := 1
endPort := 1024
timeout := time.Millisecond * 800
fmt.Printf("Scanning %s from port %d to %d\n", host, startPort, endPort)
startTime := time.Now()
results := scanPorts(host, startPort, endPort, timeout)
elapsed := time.Since(startTime)
fmt.Printf("\nScan completed in %s\n", elapsed)
fmt.Printf("Found %d open ports:\n\n", len(results))
fmt.Println("PORT\tSERVICE\tVERSION\tBANNER")
fmt.Println("----\t-------\t-------\t------")
for _, result := range results {
bannerPreview := ""
if len(result.Banner) > 30 {
bannerPreview = result.Banner(:30) + "..."
} else {
bannerPreview = result.Banner
}
fmt.Printf("%d\t%s\t%s\t%s\n",
result.Port,
result.Service,
result.Version,
bannerPreview)
}
}
Identificando serviços de corrida
Usamos duas estratégias principais para detecção de serviços:
- Identificação baseada em porta: Ao mapear para números de porta comuns (por exemplo, porta 80 é HTTP), temos um palpite provável para o serviço.
- Análise de banner: Tomamos o texto do banner e procuramos identificadores de serviço e informações de versão.
A primeira função, Grabbanner, tenta pegar a primeira resposta de um serviço. Alguns serviços como o HTTP exigem que enviemos uma solicitação e recebamos uma resposta, para a qual usamos casos de adição específicos de casos.
Detecção de versão básica
A detecção de versão é importante para a identificação de vulnerabilidades. Sempre que possível, nosso scanner analisa as faixas de serviço para puxar as informações da versão:
- Ssh: Geralmente fornece informações de versão na forma de “ssh-2. 0-openssh_7.4”
- Servidores HTTP: Geralmente responde com as informações da versão em cabeçalhos de resposta como “Servidor: Apache/2.4.29”
- Servidores de banco de dados: Pode divulgar informações da versão em suas mensagens de boas -vindas
Agora a saída retorna muito mais informações para cada porta aberta:
Scanning localhost from port 1 to 1024
Scan completed in 5.4s
Found 3 open ports:
PORT SERVICE VERSION BANNER
---- ------- ------- ------
22 SSH 2.0 SSH-2.0-OpenSSH_8.4p1 Ubuntu-6
80 HTTP Apache/2.4.41 Server: Apache/2.4.41 (Ubuntu)
443 HTTPS Unknown Connection closed by foreign...
Esta informação aprimorada é muito mais valiosa para a avaliação de vulnerabilidades.
5. Implementação de detecção de vulnerabilidade
Agora que podemos enumerar os serviços em execução e qual versão eles são, vamos implementar a detecção para as vulnerabilidades. As informações de serviço serão analisadas e comparadas com vulnerabilidades conhecidas.
Escrevendo testes de vulnerabilidade simples
Formaremos um banco de dados a partir de vulnerabilidades conhecidas com base em serviços e versões comuns. Por simplicidade, criaremos um banco de dados de vulnerabilidades no código, embora em um cenário do mundo real, um scanner provavelmente consultasse bancos de dados de vulnerabilidade externa (como CVE ou NVD).
Agora, vamos expandir nosso código para detectar vulnerabilidades:
package main
import (
"bufio"
"fmt"
"net"
"strings"
"sync"
"time"
)
type ScanResult struct {
Port int
State bool
Service string
Banner string
Version string
Vulnerabilities ()Vulnerability
}
type Vulnerability struct {
ID string
Description string
Severity string
Reference string
}
var vulnerabilityDB = ()struct {
Service string
Version string
Vulnerability Vulnerability
}{
{
Service: "SSH",
Version: "OpenSSH_7.4",
Vulnerability: Vulnerability{
ID: "CVE-2017-15906",
Description: "The process_open function in sftp-server.c in OpenSSH before 7.6 does not properly prevent write operations in read-only mode",
Severity: "Medium",
Reference: "https://nvd.nist.gov/vuln/detail/CVE-2017-15906",
},
},
{
Service: "HTTP",
Version: "Apache/2.4.29",
Vulnerability: Vulnerability{
ID: "CVE-2019-0211",
Description: "Apache HTTP Server 2.4.17 to 2.4.38 - Local privilege escalation through mod_prefork and mod_http2",
Severity: "High",
Reference: "https://nvd.nist.gov/vuln/detail/CVE-2019-0211",
},
},
{
Service: "HTTP",
Version: "Apache/2.4.41",
Vulnerability: Vulnerability{
ID: "CVE-2020-9490",
Description: "A specially crafted value for the 'Cache-Digest' header can cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41",
Severity: "High",
Reference: "https://nvd.nist.gov/vuln/detail/CVE-2020-9490",
},
},
{
Service: "MySQL",
Version: "5.7",
Vulnerability: Vulnerability{
ID: "CVE-2020-2922",
Description: "Vulnerability in MySQL Server allows unauthorized users to obtain sensitive information",
Severity: "Medium",
Reference: "https://nvd.nist.gov/vuln/detail/CVE-2020-2922",
},
},
}
func checkVulnerabilities(service, version string) ()Vulnerability {
var vulnerabilities ()Vulnerability
for _, vuln := range vulnerabilityDB {
if vuln.Service == service && strings.Contains(version, vuln.Version) {
vulnerabilities = append(vulnerabilities, vuln.Vulnerability)
}
}
return vulnerabilities
}
func grabBanner(host string, port int, timeout time.Duration) (string, error) {
target := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
return "", err
}
defer conn.Close()
conn.SetReadDeadline(time.Now().Add(timeout))
if port == 80 || port == 443 || port == 8080 || port == 8443 {
fmt.Fprintf(conn, "HEAD / HTTP/1.0\r\nHost: %s\r\n\r\n", host)
} else {
}
reader := bufio.NewReader(conn)
banner, err := reader.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(banner), nil
}
func identifyService(port int, banner string) (string, string) {
commonPorts := map(int)string{
21: "FTP",
22: "SSH",
23: "Telnet",
25: "SMTP",
53: "DNS",
80: "HTTP",
110: "POP3",
143: "IMAP",
443: "HTTPS",
3306: "MySQL",
5432: "PostgreSQL",
6379: "Redis",
8080: "HTTP-Proxy",
27017: "MongoDB",
}
service := "Unknown"
if s, exists := commonPorts(port); exists {
service = s
}
version := "Unknown"
lowerBanner := strings.ToLower(banner)
if strings.Contains(lowerBanner, "ssh") {
service = "SSH"
parts := strings.Split(banner, " ")
if len(parts) >= 2 {
version = parts(1)
}
}
if strings.Contains(lowerBanner, "http") || strings.Contains(lowerBanner, "apache") ||
strings.Contains(lowerBanner, "nginx") {
if port == 443 {
service = "HTTPS"
} else {
service = "HTTP"
}
if strings.Contains(banner, "Server:") {
parts := strings.Split(banner, "Server:")
if len(parts) >= 2 {
version = strings.TrimSpace(parts(1))
}
}
}
return service, version
}
func scanPort(host string, port int, timeout time.Duration) ScanResult {
target := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
return ScanResult{Port: port, State: false}
}
conn.Close()
banner, err := grabBanner(host, port, timeout)
service := "Unknown"
version := "Unknown"
if err == nil && banner != "" {
service, version = identifyService(port, banner)
}
vulnerabilities := checkVulnerabilities(service, version)
return ScanResult{
Port: port,
State: true,
Service: service,
Banner: banner,
Version: version,
Vulnerabilities: vulnerabilities,
}
}
func scanPorts(host string, start, end int, timeout time.Duration) ()ScanResult {
var results ()ScanResult
var wg sync.WaitGroup
resultChan := make(chan ScanResult, end-start+1)
semaphore := make(chan struct{}, 100)
for port := start; port <= end; port++ {
wg.Add(1)
go func(p int) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
result := scanPort(host, p, timeout)
resultChan <- result
}(port)
}
go func() {
wg.Wait()
close(resultChan)
}()
for result := range resultChan {
if result.State {
results = append(results, result)
}
}
return results
}
func main() {
host := "localhost"
startPort := 1
endPort := 1024
timeout := time.Second * 1
fmt.Printf("Scanning %s from port %d to %d\n", host, startPort, endPort)
startTime := time.Now()
results := scanPorts(host, startPort, endPort, timeout)
elapsed := time.Since(startTime)
fmt.Printf("\nScan completed in %s\n", elapsed)
fmt.Printf("Found %d open ports:\n\n", len(results))
fmt.Println("PORT\tSERVICE\tVERSION")
fmt.Println("----\t-------\t-------")
for _, result := range results {
fmt.Printf("%d\t%s\t%s\n",
result.Port,
result.Service,
result.Version)
if len(result.Vulnerabilities) > 0 {
fmt.Println(" Vulnerabilities:")
for _, vuln := range result.Vulnerabilities {
fmt.Printf(" (%s) %s - %s\n",
vuln.Severity,
vuln.ID,
vuln.Description)
fmt.Printf(" Reference: %s\n\n", vuln.Reference)
}
}
}
}package main
import (
"bufio"
"fmt"
"net"
"strings"
"sync"
"time"
)
type ScanResult struct {
Port int
State bool
Service string
Banner string
Version string
Vulnerabilities ()Vulnerability
}
type Vulnerability struct {
ID string
Description string
Severity string
Reference string
}
var vulnerabilityDB = ()struct {
Service string
Version string
Vulnerability Vulnerability
}{
{
Service: "SSH",
Version: "OpenSSH_7.4",
Vulnerability: Vulnerability{
ID: "CVE-2017-15906",
Description: "The process_open function in sftp-server.c in OpenSSH before 7.6 does not properly prevent write operations in read-only mode",
Severity: "Medium",
Reference: "https://nvd.nist.gov/vuln/detail/CVE-2017-15906",
},
},
{
Service: "HTTP",
Version: "Apache/2.4.29",
Vulnerability: Vulnerability{
ID: "CVE-2019-0211",
Description: "Apache HTTP Server 2.4.17 to 2.4.38 - Local privilege escalation through mod_prefork and mod_http2",
Severity: "High",
Reference: "https://nvd.nist.gov/vuln/detail/CVE-2019-0211",
},
},
{
Service: "HTTP",
Version: "Apache/2.4.41",
Vulnerability: Vulnerability{
ID: "CVE-2020-9490",
Description: "A specially crafted value for the 'Cache-Digest' header can cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41",
Severity: "High",
Reference: "https://nvd.nist.gov/vuln/detail/CVE-2020-9490",
},
},
{
Service: "MySQL",
Version: "5.7",
Vulnerability: Vulnerability{
ID: "CVE-2020-2922",
Description: "Vulnerability in MySQL Server allows unauthorized users to obtain sensitive information",
Severity: "Medium",
Reference: "https://nvd.nist.gov/vuln/detail/CVE-2020-2922",
},
},
}
func checkVulnerabilities(service, version string) ()Vulnerability {
var vulnerabilities ()Vulnerability
for _, vuln := range vulnerabilityDB {
if vuln.Service == service && strings.Contains(version, vuln.Version) {
vulnerabilities = append(vulnerabilities, vuln.Vulnerability)
}
}
return vulnerabilities
}
func grabBanner(host string, port int, timeout time.Duration) (string, error) {
target := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
return "", err
}
defer conn.Close()
conn.SetReadDeadline(time.Now().Add(timeout))
if port == 80 || port == 443 || port == 8080 || port == 8443 {
fmt.Fprintf(conn, "HEAD / HTTP/1.0\r\nHost: %s\r\n\r\n", host)
} else {
}
reader := bufio.NewReader(conn)
banner, err := reader.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(banner), nil
}
func identifyService(port int, banner string) (string, string) {
commonPorts := map(int)string{
21: "FTP",
22: "SSH",
23: "Telnet",
25: "SMTP",
53: "DNS",
80: "HTTP",
110: "POP3",
143: "IMAP",
443: "HTTPS",
3306: "MySQL",
5432: "PostgreSQL",
6379: "Redis",
8080: "HTTP-Proxy",
27017: "MongoDB",
}
service := "Unknown"
if s, exists := commonPorts(port); exists {
service = s
}
version := "Unknown"
lowerBanner := strings.ToLower(banner)
if strings.Contains(lowerBanner, "ssh") {
service = "SSH"
parts := strings.Split(banner, " ")
if len(parts) >= 2 {
version = parts(1)
}
}
if strings.Contains(lowerBanner, "http") || strings.Contains(lowerBanner, "apache") ||
strings.Contains(lowerBanner, "nginx") {
if port == 443 {
service = "HTTPS"
} else {
service = "HTTP"
}
if strings.Contains(banner, "Server:") {
parts := strings.Split(banner, "Server:")
if len(parts) >= 2 {
version = strings.TrimSpace(parts(1))
}
}
}
return service, version
}
func scanPort(host string, port int, timeout time.Duration) ScanResult {
target := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
return ScanResult{Port: port, State: false}
}
conn.Close()
banner, err := grabBanner(host, port, timeout)
service := "Unknown"
version := "Unknown"
if err == nil && banner != "" {
service, version = identifyService(port, banner)
}
vulnerabilities := checkVulnerabilities(service, version)
return ScanResult{
Port: port,
State: true,
Service: service,
Banner: banner,
Version: version,
Vulnerabilities: vulnerabilities,
}
}
func scanPorts(host string, start, end int, timeout time.Duration) ()ScanResult {
var results ()ScanResult
var wg sync.WaitGroup
resultChan := make(chan ScanResult, end-start+1)
semaphore := make(chan struct{}, 100)
for port := start; port <= end; port++ {
wg.Add(1)
go func(p int) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
result := scanPort(host, p, timeout)
resultChan <- result
}(port)
}
go func() {
wg.Wait()
close(resultChan)
}()
for result := range resultChan {
if result.State {
results = append(results, result)
}
}
return results
}
func main() {
host := "localhost"
startPort := 1
endPort := 1024
timeout := time.Second * 1
fmt.Printf("Scanning %s from port %d to %d\n", host, startPort, endPort)
startTime := time.Now()
results := scanPorts(host, startPort, endPort, timeout)
elapsed := time.Since(startTime)
fmt.Printf("\nScan completed in %s\n", elapsed)
fmt.Printf("Found %d open ports:\n\n", len(results))
fmt.Println("PORT\tSERVICE\tVERSION")
fmt.Println("----\t-------\t-------")
for _, result := range results {
fmt.Printf("%d\t%s\t%s\n",
result.Port,
result.Service,
result.Version)
if len(result.Vulnerabilities) > 0 {
fmt.Println(" Vulnerabilities:")
for _, vuln := range result.Vulnerabilities {
fmt.Printf(" (%s) %s - %s\n",
vuln.Severity,
vuln.ID,
vuln.Description)
fmt.Printf(" Reference: %s\n\n", vuln.Reference)
}
}
}
}
Combinação baseada em versão de vulnerabilidades
Temos uma abordagem ingênua de correspondência de versão para detecção de vulnerabilidades:
- Correspondência direta: Aqui, correspondemos ao tipo de serviço e versão ao nosso banco de dados de vulnerabilidades.
- Correspondência parcial: Para correspondência de versão vulnerável, realizamos verificações de contenção na sequência da versão, permitindo que identifiquem sistemas vulneráveis, mesmo que a string de versão contenha informações extras.
Em um scanner real, essa correspondência seria mais complexa, representando:
- Bancos de versão (ou seja, versões 2.4.0 a 2.4.38 são afetadas)
- Vulnerabilidades específicas da configuração
- Questões específicas do sistema operacional
- Comparações de versão mais sutis
Relatando o que encontramos
Relatar os resultados é a última etapa da detecção de vulnerabilidades e que precisa ser feita em um formato conciso e acionável. Nosso scanner agora:
- Lista todas as portas abertas com informações de serviço e versão
- Para cada serviço vulnerável, exibe:
- O ID da vulnerabilidade (por exemplo, número CVE)
- Uma descrição da vulnerabilidade
- Classificação de gravidade
- Link de referência para mais informações
Saída de amostra:
Scanning localhost from port 1 to 1024
Scan completed in 6.2s
Found 3 open ports:
PORT SERVICE VERSION
---- ------- -------
22 SSH OpenSSH_7.4p1
Vulnerabilities:
(Medium) CVE-2017-15906 - The process_open function in sftp-server.c in OpenSSH before 7.6 does not properly prevent write operations in read-only mode
Reference: https://nvd.nist.gov/vuln/detail/CVE-2017-15906
80 HTTP Apache/2.4.41
Vulnerabilities:
(High) CVE-2020-9490 - A specially crafted value for the 'Cache-Digest' header can cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41
Reference: https://nvd.nist.gov/vuln/detail/CVE-2020-9490
443 HTTPS Unknown
Esse dado de vulnerabilidade completo orienta os especialistas em segurança cibernética a identificar prontamente e classificar as preocupações de segurança que exigem resolução.
Toques e uso finais
Agora você tem um scanner de vulnerabilidade básico com detecção de serviços e correspondência de vulnerabilidades; Agora, vamos polir um pouco para que seja mais prático usar no mundo real.
Argumentos da linha de comando
Nosso scanner deve ser configurável por meio de sinalizadores de linha de comando que podem definir alvos, intervalos de porta e opções de digitalização. Isso é simples com o pacote de bandeira do Go.
Em seguida, vamos adicionar argumentos de linha de comando:
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"net"
"os"
"strings"
"sync"
"time"
)
type ScanResult struct {
Port int
State bool
Service string
Banner string
Version string
Vulnerabilities ()Vulnerability
}
type Vulnerability struct {
ID string
Description string
Severity string
Reference string
}
var vulnerabilityDB = ()struct {
Service string
Version string
Vulnerability Vulnerability
}{
}
func main() {
hostPtr := flag.String("host", "", "Target host to scan (required)")
startPortPtr := flag.Int("start", 1, "Starting port number")
endPortPtr := flag.Int("end", 1024, "Ending port number")
timeoutPtr := flag.Int("timeout", 1000, "Timeout in milliseconds")
concurrencyPtr := flag.Int("concurrency", 100, "Number of concurrent scans")
formatPtr := flag.String("format", "text", "Output format: text, json, or csv")
verbosePtr := flag.Bool("verbose", false, "Show verbose output including banners")
outputFilePtr := flag.String("output", "", "Output file (default is stdout)")
flag.Parse()
if *hostPtr == "" {
fmt.Println("Error: host is required")
flag.Usage()
os.Exit(1)
}
if *startPortPtr < 1 || *startPortPtr > 65535 {
fmt.Println("Error: starting port must be between 1 and 65535")
os.Exit(1)
}
if *endPortPtr < 1 || *endPortPtr > 65535 {
fmt.Println("Error: ending port must be between 1 and 65535")
os.Exit(1)
}
if *startPortPtr > *endPortPtr {
fmt.Println("Error: starting port must be less than or equal to ending port")
os.Exit(1)
}
timeout := time.Duration(*timeoutPtr) * time.Millisecond
var outputFile *os.File
var err error
if *outputFilePtr != "" {
outputFile, err = os.Create(*outputFilePtr)
if err != nil {
fmt.Printf("Error creating output file: %v\n", err)
os.Exit(1)
}
defer outputFile.Close()
} else {
outputFile = os.Stdout
}
fmt.Fprintf(outputFile, "Scanning %s from port %d to %d\n", *hostPtr, *startPortPtr, *endPortPtr)
startTime := time.Now()
var results ()ScanResult
var wg sync.WaitGroup
resultChan := make(chan ScanResult, *endPortPtr-*startPortPtr+1)
semaphore := make(chan struct{}, *concurrencyPtr)
for port := *startPortPtr; port <= *endPortPtr; port++ {
wg.Add(1)
go func(p int) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
result := scanPort(*hostPtr, p, timeout)
resultChan <- result
}(port)
}
go func() {
wg.Wait()
close(resultChan)
}()
for result := range resultChan {
if result.State {
results = append(results, result)
}
}
elapsed := time.Since(startTime)
switch *formatPtr {
case "json":
outputJSON(outputFile, results, elapsed)
case "csv":
outputCSV(outputFile, results, elapsed, *verbosePtr)
default:
outputText(outputFile, results, elapsed, *verbosePtr)
}
}
func outputText(w *os.File, results ()ScanResult, elapsed time.Duration, verbose bool) {
fmt.Fprintf(w, "\nScan completed in %s\n", elapsed)
fmt.Fprintf(w, "Found %d open ports:\n\n", len(results))
if len(results) == 0 {
fmt.Fprintf(w, "No open ports found.\n")
return
}
fmt.Fprintf(w, "PORT\tSERVICE\tVERSION\n")
fmt.Fprintf(w, "----\t-------\t-------\n")
for _, result := range results {
fmt.Fprintf(w, "%d\t%s\t%s\n",
result.Port,
result.Service,
result.Version)
if verbose {
fmt.Fprintf(w, " Banner: %s\n", result.Banner)
}
if len(result.Vulnerabilities) > 0 {
fmt.Fprintf(w, " Vulnerabilities:\n")
for _, vuln := range result.Vulnerabilities {
fmt.Fprintf(w, " (%s) %s - %s\n",
vuln.Severity,
vuln.ID,
vuln.Description)
fmt.Fprintf(w, " Reference: %s\n\n", vuln.Reference)
}
}
}
}
func outputJSON(w *os.File, results ()ScanResult, elapsed time.Duration) {
output := struct {
ScanTime string `json:"scan_time"`
ElapsedTime string `json:"elapsed_time"`
TotalPorts int `json:"total_ports"`
OpenPorts int `json:"open_ports"`
Results ()ScanResult `json:"results"`
}{
ScanTime: time.Now().Format(time.RFC3339),
ElapsedTime: elapsed.String(),
TotalPorts: 0,
OpenPorts: len(results),
Results: results,
}
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
encoder.Encode(output)
}
func outputCSV(w *os.File, results ()ScanResult, elapsed time.Duration, verbose bool) {
fmt.Fprintf(w, "Port,Service,Version,Vulnerability ID,Severity,Description\n")
for _, result := range results {
if len(result.Vulnerabilities) == 0 {
fmt.Fprintf(w, "%d,%s,%s,,,\n",
result.Port,
escapeCSV(result.Service),
escapeCSV(result.Version))
} else {
for _, vuln := range result.Vulnerabilities {
fmt.Fprintf(w, "%d,%s,%s,%s,%s,%s\n",
result.Port,
escapeCSV(result.Service),
escapeCSV(result.Version),
escapeCSV(vuln.ID),
escapeCSV(vuln.Severity),
escapeCSV(vuln.Description))
}
}
}
fmt.Fprintf(w, "\n# Scan completed in %s, found %d open ports\n",
elapsed, len(results))
}
func escapeCSV(s string) string {
if strings.Contains(s, ",") || strings.Contains(s, "\"") || strings.Contains(s, "\n") {
return "\"" + strings.ReplaceAll(s, "\"", "\"\"") + "\""
}
return s
}
Formatação de saída
Nosso scanner agora pode produzir para três formatos:
- Texto: Fácil de ler, fácil de escrever, ótimo para uso interativo.
- JSON: Saída estruturada útil para processamento e integração de máquinas com outras ferramentas.
- CSV: Um formato compatível com a planilha para análise e relatório.
O texto de saída também fornece mais informações, como informações de banner bruto se o sinalizador verbose estiver definido. Isso também é útil para depuração ou análise profunda.
Exemplo de uso e resultados
Então, aqui estão algumas possibilidades se você quiser usar nosso scanner para diferentes ocasiões:
Digitalização básica de um único host:
$ go run main.go -host example.com
Digitalize um intervalo de portas específico:
$ go run main.go -host example.com -start 80 -end 443
Salvar resultados em um arquivo JSON:
$ go run main.go -host example.com -format json -output results.json
Digitalização detalhada com aumento de tempo limite:
$ go run main.go -host example.com -verbose -timeout 2000
Digitalizar com maior concorrência para obter resultados mais rápidos:
$ go run main.go -host example.com -concurrency 200
Exemplo de saída de texto:
Scanning example.com from port 1 to 1024
Scan completed in 12.6s
Found 3 open ports:
PORT SERVICE VERSION
---- ------- -------
22 SSH OpenSSH_7.4p1
Vulnerabilities:
(Medium) CVE-2017-15906 - The process_open function in sftp-server.c in OpenSSH before 7.6 does not properly prevent write operations in read-only mode
Reference: https://nvd.nist.gov/vuln/detail/CVE-2017-15906
80 HTTP Apache/2.4.41
Vulnerabilities:
(High) CVE-2020-9490 - A specially crafted value for the 'Cache-Digest' header can cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41
Reference: https://nvd.nist.gov/vuln/detail/CVE-2020-9490
443 HTTPS nginx/1.18.0
Exemplo de saída JSON:
{
"scan_time": "2025-03-18T14:30:00Z",
"elapsed_time": "12.6s",
"total_ports": 1024,
"open_ports": 3,
"results": (
{
"Port": 22,
"State": true,
"Service": "SSH",
"Banner": "SSH-2.0-OpenSSH_7.4p1",
"Version": "OpenSSH_7.4p1",
"Vulnerabilities": (
{
"ID": "CVE-2017-15906",
"Description": "The process_open function in sftp-server.c in OpenSSH before 7.6 does not properly prevent write operations in read-only mode",
"Severity": "Medium",
"Reference": "https://nvd.nist.gov/vuln/detail/CVE-2017-15906"
}
)
},
{
"Port": 80,
"State": true,
"Service": "HTTP",
"Banner": "HTTP/1.1 200 OK\r\nServer: Apache/2.4.41",
"Version": "Apache/2.4.41",
"Vulnerabilities": (
{
"ID": "CVE-2020-9490",
"Description": "A specially crafted value for the 'Cache-Digest' header can cause a heap overflow in Apache HTTP Server 2.4.0-2.4.41",
"Severity": "High",
"Reference": "https://nvd.nist.gov/vuln/detail/CVE-2020-9490"
}
)
},
{
"Port": 443,
"State": true,
"Service": "HTTPS",
"Banner": "HTTP/1.1 200 OK\r\nServer: nginx/1.18.0",
"Version": "nginx/1.18.0",
"Vulnerabilities": ()
}
)
}
Construímos um scanner de vulnerabilidade de rede robusto em Go que demonstra a adequação do idioma para ferramentas de segurança. Nosso scanner abre rapidamente portas, identifica serviços em execução e determina se as vulnerabilidades conhecidas estão ou não presentes.
Oferece informações úteis sobre serviços em execução em uma rede, incluindo múltiplos threading, impressão digital de serviço e vários formatos de saída.
Lembre -se de que ferramentas como um scanner devem ser usadas apenas em parâmetros éticos e legais, com autorização adequada para digitalizar os sistemas de destino. Quando conduzido com responsabilidade, a varredura regular de vulnerabilidades é um aspecto crítico da boa postura de segurança que pode ajudar a proteger seus sistemas contra ameaças.
Você pode encontrar o código -fonte completo para este projeto em Girub