Fermer

mars 31, 2025

Construire un scanner de vulnérabilité de réseau avec GO –

Construire un scanner de vulnérabilité de réseau avec GO –


Les tests de pénétration permettent aux organisations de cibler les faiblesses potentielles de sécurité dans un réseau et de fournir la nécessité de corriger les vulnérabilités avant d’être compromises par un acteur malveillant.

Dans cet article, nous allons créer un scanner de vulnérabilité de réseau simple et raisonnablement robuste à l’aide de Go, un langage très adapté à la programmation réseau car il est conçu avec une concurrence à l’esprit et a une excellente bibliothèque standard.

1. Configuration de notre projet

Créer un scanner de vulnérabilité

Nous voulons créer un outil CLI simple qui serait en mesure de scanner un réseau d’hôtes, de trouver des ports ouverts, d’exécuter des services et de découvrir une éventuelle vulnérabilité. Le scanner va être très simple à démarrer, mais deviendra de plus en plus capable lorsque nous superposons sur les fonctionnalités.

Donc, d’abord, nous créerons un nouveau projet Go:

mkdir goscan
cd goscan
go mod init github.com/yourusername/goscan

Cela initialise un nouveau module GO pour notre projet, qui nous aidera à gérer les dépendances.

Configuration des packages et de l’environnement

Pour notre scanner, nous tirons parti de plusieurs forfaits GO:

package main

import (
    "fmt"
    "net"
    "os"
    "strconv"
    "sync"
    "time"
)

func main() {
    fmt.Println("GoScan - Network Vulnerability Scanner")
}

Ce n’est que notre configuration initiale. Ce sera suffisant pour certaines fonctionnalités initiales, mais nous ajouterons plus d’importations à la demande. Désormais, d’autres packages de bibliothèque standard comme Net prendront soin de faire la plupart des réseaux dont nous avons besoin et de synchronisation feront de la concurrence, etc.

Considérations et risques éthiques avec la numérisation du réseau

Maintenant, avant de passer à la mise en œuvre, nous devons aborder certaines considérations éthiques autour de la numérisation du réseau. La numérisation ou l’énumération du réseau non autorisé est illégal dans de nombreuses régions du monde et est traité comme un vecteur pour une cyberattaque. Vous devez toujours suivre ces règles:

  1. Autorisation: Numérisez uniquement des réseaux et systèmes nonce que vous possédez ou ayez une autorisation explicite pour scanner.
  2. Portée: Définissez une portée claire pour votre numérisation et ne la dépassez pas.
  3. Timing: N’allez pas pour l’hyper-scanning qui peut abattre des services ou augmenter les alertes de sécurité.
  4. Divulgation: Si vous découvrez des vulnérabilités, veuillez le faire de manière responsable en les rapportant aux propriétaires de systèmes appropriés.
  5. Conformité légale: Comprendre et respecter les lois locales régissant la numérisation du réseau.

L’utilisation abusive des outils de numérisation peut entraîner une action en justice, des dommages au système ou un déni de service accidentel. Notre scanner comprendra des garanties comme la limitation des taux, mais la responsabilité incombe par l’utilisateur à l’utiliser de manière éthique.

2. Scanner de ports simples

L’évaluation de la vulnérabilité est basée sur la numérisation du port. Les services vulnérables potentiels offerts sur chacun de ces ports ouverts sont les informations que nous recherchons. Maintenant, écrivons un simple scanner de port en Go.

Implémentation de bas niveau de la numérisation des ports

Analyse du port: essayez d’établir une connexion à chaque port possible d’un hôte cible. Si la connexion réussit, le port est ouvert; S’il échoue, le port est fermé ou filtré. Pour cette fonctionnalité, le package Net de Go nous a couvert.

Alors, voici notre version d’un simple scanner de port:

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")
}

Utilisation du package net

Le code ci-dessus utilise le package GO Net, qui fournit des interfaces et fonctions d’E / S du réseau. Alors, quelles sont les pièces principales?

  1. net.dialtimeout: Cette fonction essaie de se connecter à l’adresse réseau TCP avec un temps mort. Il renvoie une connexion et une erreur, le cas échéant.
  2. Gestion des connexions: S’il se connecte sans problème, nous savons qu’il est ouvert et nous fermons immédiatement la connexion pour ouvrir des ressources.
  3. Paramètre de délai: Nous spécifions un délai d’expiration pour éviter de s’accrocher à tous les ports ouverts filtrés. Deux secondes est une bonne valeur initiale, mais cela peut être réglé en fonction des conditions du réseau.

Tester notre premier scan

Maintenant, exécutons notre simple scanner contre notre Host local, où nous pourrions avoir des services en cours d’exécution.

  1. Enregistrez le code dans un fichier nommé main.go
  2. Courir avec go run main.go

Cela montrera quels ports locaux sont ouverts. Sur une machine de développement normale, vous pouvez avoir 80 (HTTP), 443 (HTTPS) ou n’importe quel nombre de ports de base de données utilisés en fonction des services que vous avez.

Voici un exemple de sortie que vous pourriez obtenir:

Scanning host: localhost
Port 22 is open
Port 80 is open
Port 443 is open
Scan complete

L’utilisation de ce scanner de base fonctionne, mais il est livré avec de grands inconvénients:

  1. Vitesse: Il est douloureusement lent car il scanne les ports séquentiellement.
  2. Information: Nous dit simplement si un port est ouvert, pas d’informations de service.
  3. Gamme limitée: Nous allons seulement scanner les 1024 premiers ports.

Ces restrictions rendent notre scanner impraticable d’être utilisé dans le monde réel.

3. L’améliorer à partir d’ici: numérisation multi-thread

Pourquoi la première version est lente

Notre premier scanner de port fonctionne, bien qu’il soit douloureusement lent d’être utilisable. Le problème est sa méthode séquentielle – sonder un port à la fois. Lorsqu’un hôte a beaucoup de ports fermés / filtrés, nous perdons du temps à attendre une connexion à un temps Out sur chaque port avant de passer à l’autre.

Pour vous montrer le problème, jetons un coup d’œil au moment de notre scanner de base:

  • Le pire des cas pour la numérisation des 1024 premiers ports prendrait un maximum de 2048 secondes (plus de 34 minutes) avec 2 seconds délai
  • Mais même lorsque les connexions aux ports fermées échouent immédiatement, cette méthode est inefficace en raison de la latence du réseau.

Cette approche un par un est un goulot d’étranglement pour tout outil de balayage de vulnérabilité réel.

Ajout de la prise en charge de filetage

GO est particulièrement bon en concurrence en utilisant des goroutines et des canaux. Ainsi, nous tirons parti de ces fonctionnalités pour essayer de scanner plusieurs ports à la fois, ce qui augmente considérablement les performances.

Maintenant, créons un scanner de port 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)
    }
}

Résultats de plusieurs threads

Maintenant, jetons un coup d’œil aux gains de performances ainsi qu’aux mécanismes de concurrence que nous avons ajoutés à notre scanner amélioré:

  1. Goroutins: Pour rendre le numérisation efficace, nous lançons un goroutine pour chaque port que nous devons scanner, de sorte que pendant que nous vérifions un port, nous pouvons vérifier les autres ports simultanément.
  2. Groupe d’attente: La synchronisation. Waitgroupas Nous inductons des goroutines, nous voulons attendre leur achèvement. WaitGroup nous aide à suivre tous les Goroutines en cours d’exécution et à attendre qu’ils se terminent.
  3. Canal de résultat: Nous créons un canal de tampons pour les résultats de tous les Goroutines dans l’ordre.
  4. Modèle de sémaphore: Un sémaphore est utilisé, implémenté à l’aide d’un canal, qui limite le nombre de scans autorisés en parallèle. C’est ce qui nous empêche de submerger le système cible réel ou même notre propre machine avec autant de connexion ouverte.
  5. Timeout réduit: Puisque nous exécutons bon nombre de ces scans de manière parallèle, nous utilisons un délai inférieur.

L’écart de performance est substantiel. Ainsi, lorsque nous implémentons cela, il peut nous permettre de numériser 1024 ports en quelques minutes, et certainement moins d’une demi-heure.

Exemple de sortie:

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

L’approche multithread évolue très bien pour les gammes de ports plus grandes et plusieurs hôtes. Le modèle de sémaphore garantit que nous ne manquons pas de ressources système malgré la numérisation de plus d’un millier de ports.

4. Ajout de détection de service

Maintenant que nous avons un scanner de port rapide et efficace, la prochaine étape consiste à savoir quels services fonctionnent sur ces ports ouverts. Ceci est communément appelé «empreinte digitale» ou «saisie de bannières», un processus par lequel nous nous connectons aux ports ouverts et examinons les données renvoyées.

Mise en œuvre de la saisie des bannières

La saisie des bannières, c’est lorsque nous ouvrons un service et lisons la réponse (bannière), il nous envoie. C’est donc un bon moyen d’identifier si quelque chose fonctionne, car de nombreux services s’identifient dans ces bannières.

Ajoutons maintenant la viande de bannière à notre 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)
    }
}

Identifier les services de course

Nous utilisons deux stratégies principales pour la détection des services:

  1. Identification basée sur le port: En mappant sur des numéros de port communs (par exemple, le port 80 est HTTP), nous avons probablement une supposition pour le service.
  2. Analyse de bannière: Nous prenons le texte de la bannière et recherchons des identifiants de service et des informations de version.

La première fonction, Grabbanner, essaie de saisir la première réponse d’un service. Certains services tels que HTTP nous obligent à envoyer une demande et à recevoir une réponse, pour laquelle nous utilisons Ajouter des cas spécifiques au cas.

Détection de version de base

La détection de version est importante pour l’identification des vulnérabilités. Dans la mesure du possible, notre scanner analyse les bannières de service pour extraire les informations de version:

  1. Ssh: Fournit généralement des informations sur la version sous la forme de « SSH-2. 0-OpenSSH_7.4 »
  2. Serveurs HTTP: Répondez généralement avec leurs informations de version dans des en-têtes de réponse tels que «Server: Apache / 2.4.29»
  3. Serveurs de base de données: Pourrait divulguer les informations de version dans leurs messages de bienvenue

Maintenant, la sortie renvoie beaucoup plus d’informations pour chaque port ouvert:

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...

Ces informations améliorées sont beaucoup plus précieuses pour l’évaluation de la vulnérabilité.

5. Implémentation de détection de vulnérabilité

Maintenant que nous pouvons énumérer les services en cours d’exécution et quelle version ils sont, nous allons mettre en œuvre la détection pour les vulnérabilités. Les informations de service seront analysées et comparées aux vulnérabilités connues.

Écrire des tests de vulnérabilité simples

Nous formerons une base de données à partir de vulnérabilités connues basées sur des services et des versions communs. Pour plus de simplicité, nous créerons une base de données de vulnérabilité en code, bien que dans un scénario du monde réel, un scanner interrogerait très probablement des bases de données de vulnérabilité externes (telles que CVE ou NVD).

Maintenant, élargissons notre code pour détecter les vulnérabilités:

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)
            }
        }
    }
}

Correspondance des vulnérabilités basée sur les versions

Nous avons une approche de correspondance de version naïve pour la détection de vulnérabilité:

  1. Correspondance directe: Ici, nous correspondons au type de service et à la version à notre base de données de vulnérabilité.
  2. Correspondance partielle: Pour la correspondance des versions vulnérables, nous effectuons des vérifications de confinement sur la chaîne de version, ce qui nous permet d’identifier les systèmes vulnérables même si la chaîne de version contient des informations supplémentaires.

Dans un vrai scanner, cette correspondance serait plus complexe, ce qui représente:

  • Les gammes de versions (IE Les versions 2.4.0 à 2.4.38 sont affectées)
  • Vulnérabilités spécifiques à la configuration
  • Problèmes spécifiques au système d’exploitation
  • Des comparaisons de versions plus nuancées

Signaler ce que nous trouvons

La déclaration des résultats est la dernière étape de la détection de vulnérabilité et cela doit être fait dans un format concis et exploitable. Notre scanner maintenant:

  1. Répertorie tous les ports ouverts avec des informations de service et de version
  2. Pour chaque service vulnérable, affiche:
    • L’ID de vulnérabilité (par exemple, numéro CVE)
    • Une description de la vulnérabilité
    • Cote de gravité
    • Lien de référence pour plus d’informations

Exemple de sortie:

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

Cette données approfondie des données de vulnérabilité guide les spécialistes de la cybersécurité pour identifier rapidement et classer les problèmes de sécurité qui nécessitent une résolution.

Touches finales et utilisation

Vous avez maintenant un scanner de vulnérabilité de base avec détection de service et correspondance de vulnérabilité; Maintenant, polissons-le un peu pour qu’il soit plus pratique à utiliser dans le monde réel.

Arguments de ligne de commande

Notre scanner doit être configurable via des drapeaux de ligne de commande qui peuvent définir des cibles, des gammes de port et des options de numérisation. C’est simple avec le package de drapeau de Go.

Ensuite, ajoutons des arguments en ligne de commande:

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
}

Formatage de sortie

Notre scanner peut désormais passer à trois formats:

  1. Texte: Facile à lire, facile à écrire, idéal pour une utilisation interactive.
  2. Json: Sortie structurée utile pour le traitement et l’intégration de la machine avec d’autres outils.
  3. CSV: Un format compatible avec une feuille de calcul pour l’analyse et les rapports.

Le texte de sortie fournit également plus d’informations telles que les informations de bannière brutes si l’indicateur verbeux est défini. Ceci est également pratique pour le débogage ou l’analyse en profondeur.

Exemple d’utilisation et de résultats

Donc, voici quelques possibilités si vous allez utiliser notre scanner pour différentes occasions:

Analyse de base d’un seul hôte:

$ go run main.go -host example.com

Scannez une plage de port spécifique:

$ go run main.go -host example.com -start 80 -end 443

Enregistrer les résultats dans un fichier JSON:

$ go run main.go -host example.com -format json -output results.json

Scan verbeux avec un temps mort accru:

$ go run main.go -host example.com -verbose -timeout 2000

Scanner avec une concurrence plus élevée pour des résultats plus rapides:

$ go run main.go -host example.com -concurrency 200

Exemple de sortie de texte:

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

Exemple de sortie 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": []
    }
  ]
}

Nous avons construit un scanner de vulnérabilité réseau robuste dans GO qui démontre l’adéquation du langage pour les outils de sécurité. Notre scanner ouvre rapidement les ports, identifie les services qui les exécutent et déterminent si des vulnérabilités connues sont présentes ou non.

Il offre des informations utiles sur les services exécutés sur un réseau, y compris le multi-threading, les empreintes digitales de service et divers formats de sortie.

Gardez à l’esprit que des outils comme un scanner ne doivent être utilisés que dans les paramètres éthiques et juridiques, avec une autorisation appropriée pour scanner les systèmes cibles. Lorsqu’il est effectué de manière responsable, la numérisation régulière de la vulnérabilité est un aspect essentiel d’une bonne posture de sécurité qui peut aider à protéger vos systèmes contre les menaces.

Vous pouvez trouver le code source complet de ce projet sur Girub




Source link