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:
- Autorisation: Numérisez uniquement des réseaux et systèmes nonce que vous possédez ou ayez une autorisation explicite pour scanner.
- Portée: Définissez une portée claire pour votre numérisation et ne la dépassez pas.
- Timing: N’allez pas pour l’hyper-scanning qui peut abattre des services ou augmenter les alertes de sécurité.
- 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.
- 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?
- 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.
- 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.
- 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.
- Enregistrez le code dans un fichier nommé
main.go
- 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:
- Vitesse: Il est douloureusement lent car il scanne les ports séquentiellement.
- Information: Nous dit simplement si un port est ouvert, pas d’informations de service.
- 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é:
- 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.
- 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.
- Canal de résultat: Nous créons un canal de tampons pour les résultats de tous les Goroutines dans l’ordre.
- 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.
- 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:
- 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.
- 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:
- Ssh: Fournit généralement des informations sur la version sous la forme de « SSH-2. 0-OpenSSH_7.4 »
- 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»
- 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é:
- Correspondance directe: Ici, nous correspondons au type de service et à la version à notre base de données de vulnérabilité.
- 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:
- Répertorie tous les ports ouverts avec des informations de service et de version
- 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:
- Texte: Facile à lire, facile à écrire, idéal pour une utilisation interactive.
- Json: Sortie structurée utile pour le traitement et l’intégration de la machine avec d’autres outils.
- 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