Skverbimosi bandymai suteikia galimybę organizacijoms nukreipti galimus tinklo saugumo trūkumus ir suteikti poreikį ištaisyti pažeidžiamumus, kol joms kenkia kenkėjiškas veikėjas.
Šiame straipsnyje mes sukursime paprastą, pagrįstai tvirtą, tinklo pažeidžiamumo skaitytuvą naudojant „Go“ – kalbą, kuri labai tinka tinklo programavimui, nes jis yra sukurtas atsižvelgiant į suderinamumą ir turi puikią standartinę biblioteką.
1. Mūsų projekto nustatymas
Sukurkite pažeidžiamumo skaitytuvą
Mes norime sukurti paprastą CLI įrankį, kuris galėtų nuskaityti pagrindinių kompiuterių tinklą, rasti atvirus prievadus, paleisti paslaugas ir atrasti galimą pažeidžiamumą. Skaitytuvo pradžią bus labai paprasta, tačiau jis vis labiau pajėgus, kai mes apskaičiuojame funkcijas.
Taigi, pirmiausia sukursime naują „Go“ projektą:
mkdir goscan
cd goscan
go mod init github.com/yourusername/goscan
Tai inicijuoja naują mūsų projekto modulį, kuris padės mums valdyti priklausomybes.
Paketų ir aplinkos konfigūravimas
Mūsų skaitytuvui pasinaudosime keliais „Go“ pakuotėmis:
package main
import (
"fmt"
"net"
"os"
"strconv"
"sync"
"time"
)
func main() {
fmt.Println("GoScan - Network Vulnerability Scanner")
}
Tai tik mūsų pradinė sąranka. To pakaks kai kurioms pradinėms funkcijoms, tačiau mes pridėsime daugiau importo pagal paklausą. Dabar kiti standartiniai bibliotekų paketai, tokie kaip „Net“, pasirūpins, kad atliktų didžiąją dalį mums reikalingų tinklų, kurie bus suderinti, ir pan.
Etiniai svarstymai ir rizika naudojant tinklo nuskaitymą
Dabar, prieš pradėdami įgyvendinti, turėtume paliesti kai kuriuos etinius aspektus, susijusius su tinklo nuskaitymu. Neleistinas tinklo nuskaitymas ar sąrašas daugelyje pasaulio vietų yra neteisėtas ir yra traktuojamas kaip kibernetinės atakos vektorius. Visada turite laikytis šių taisyklių:
- Leidimas: Tik nuskaitykite ne CE tinklus ir sistemas, kurios jums priklauso arba turi aiškų leidimą nuskaityti.
- Taikymo sritis: Apibrėžkite aiškią nuskaitymo apimtį ir neviršykite jos.
- Laikas: Neikite hiper-nuskaitymo, kuris gali sumažinti paslaugas ar padidinti saugumo įspėjimus.
- Atskleidimas: Jei atrandate pažeidžiamumus, darykite tai atsakingai, pranešdami apie juos atitinkamiems sistemos savininkams.
- Teisinis laikymasis: Supraskite ir laikykitės vietinių įstatymų, reglamentuojančių tinklo skenavimą.
Netinkamas nuskaitymo priemonių naudojimas gali sukelti teisinius veiksmus, sistemos pažeidimus ar atsitiktinį paslaugų atsisakymą. Mūsų skaitytuvas apims tokias apsaugos priemones kaip tarifų ribojimas, tačiau atsakomybė galiausiai tenka vartotojui etiškai įdarbinti.
2. Paprastas prievado skaitytuvas
Pažeidžiamumo vertinimas grindžiamas prievadų nuskaitymu. Galimos pažeidžiamos paslaugos, kurios siūlomos kiekviename iš šių atvirų uostų, yra informacija, kurios mes ieškome. Dabar parašykime paprastą prievado skaitytuvą.
Žemo lygio prievadų nuskaitymo įgyvendinimas
Prievado nuskaitymas: pabandykite užmegzti ryšį su kiekvienu įmanomu tikslinio kompiuterio prievadu. Jei ryšys pavyksta, prievadas atidarytas; Jei jis nepavyksta, uostas uždarytas arba filtruojamas. Dėl šios funkcijos „Go“ tinklo paketas mus apžvelgė.
Taigi, čia yra mūsų paprasto prievado skaitytuvo versija:
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")
}
Naudojant tinklo paketą
Aukščiau pateiktame kode naudojamas „Go Net“ paketas, kuris tiekia tinklo I/O sąsajas ir funkcijas. Taigi, kokie yra pagrindiniai kūriniai?
- net.dialtimeout: Ši funkcija bando prisijungti prie TCP tinklo adreso su laiku. Tai grąžina ryšį ir klaidą, jei tokių yra.
- Ryšio tvarkymas: Jei jis jungiasi be išleidimo, mes žinome, kad jis yra atidarytas, ir mes iškart uždarome ryšį, kad atidarytume išteklius.
- Laiko parametras: Mes nurodome laiką, kad būtų išvengta pakabinimo ant filtruojamų atvirų prievadų. Dvi sekundės yra gera pradinė vertė, tačiau tai galima suderinti pagal tinklo sąlygas.
Pirmojo nuskaitymo išbandymas
Dabar paleisime savo paprastą skaitytuvą prieš „Localhost“, kur mums gali būti teikiamos tam tikros paslaugos.
- Išsaugokite kodą į pavadintą failą
main.go
- Paleiskite tai su
go run main.go
Tai parodys, kokie vietiniai uostai yra atviri. Įprastoje „Dev“ kompiuteryje gali būti 80 (HTTP), 443 (HTTPS) ar bet kokį naudojamų duomenų bazių prievadų skaičių, atsižvelgiant į tai, kokias paslaugas turite.
Štai keletas pavyzdžių išvesties, kurią galite gauti:
Scanning host: localhost
Port 22 is open
Port 80 is open
Port 443 is open
Scan complete
Naudojant šį pagrindinį skaitytuvo naudojimą, tačiau jis pateikia keletą didelių trūkumų:
- Greitis: Tai skausmingai lėtas, nes jis nuskaito prievadus iš eilės.
- Informacija: Tiesiog pasakykite mums, ar uostas yra atidarytas, nėra informacijos apie paslaugas.
- Ribotas diapazonas: Mes nuskaitysime tik pirmuosius 1024 prievadus.
Dėl šių apribojimų mūsų skaitytuvas nepraktiškas, kurį reikia naudoti tikrame pasaulyje.
3. Patobulinimas iš čia: daugiapakopis nuskaitymas
Kodėl pirmoji versija yra lėta
Mūsų pirmasis prievadų skaitytuvas veikia, nors skaudžiai lėtai yra naudojamas. Šis klausimas yra jo nuoseklus metodas – tikrinant vieną prievadą vienu metu. Kai šeimininkas turi daug uždarų/filtruotų prievadų, mes eikvojame laiką laukdami laiko prie kiekvieno prievado laiko, prieš pradėdami judėti į kitą.
Norėdami parodyti jums problemą, pažvelkime į mūsų pagrindinio skaitytuvo laiką:
- Blogiausias atvejis, kai nuskaito pirmuosius 1024 prievadus
- Bet net tada, kai jungtys su uždarais prievadais iškart sugenda, šis metodas yra neveiksmingas dėl tinklo latencijos.
Šis vienas po kito požiūris yra bet kokio tikro pažeidžiamumo nuskaitymo įrankio kliūtis.
Pridedant sriegio palaikymą
„Go“ yra ypač gerai suderinamas naudojant „Goroutines“ ir kanalus. Taigi, mes pasinaudojame šiomis funkcijomis, norėdami nuskaityti kelis prievadus vienu metu, o tai žymiai padidina našumą.
Dabar sukurkime daugiapakopį prievadų skaitytuvą:
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)
}
}
Rezultatai iš kelių gijų
Dabar pažvelkime į našumo padidėjimą ir suderinamumo mechanizmus, kuriuos pridėjome prie patobulinto skaitytuvo:
- Gorutinai: Norėdami, kad nuskaitymas būtų efektyvus, mes sukuriame kiekvieno prievado, kurį mums reikia nuskaityti, gorutine, taigi, kol tikriname vieną prievadą, galime vienu metu patikrinti kitus prievadus.
- Waiting Group: Sinchronizavimas. „WaitGroupas“, kurią sukelia gorutinai, norime laukti jų pabaigos. „WaitGroup“ padeda mums sekti visus veikiančius gorutinus ir laukti, kol jie bus baigti.
- Rezultato kanalas: Mes sukuriame „Buffer“ kanalą, skirtą rezultatams iš visų „Goroutines“.
- Semaforo modelis: Naudojamas semaforas, įgyvendinamas naudojant kanalą, kuris riboja lygiagrečiai leidžiamų nuskaitymų skaičių. Tai neleidžia mums užvaldyti tikrosios tikslinės sistemos ar net mūsų pačių mašinos, kai tiek daug ryšio yra atidarytas.
- Sumažintas laikas: Kadangi daugelį šių nuskaitymų atliekame lygiagrečiai, mes naudojame mažesnį laiką.
Spektaklio atotrūkis yra didelis. Taigi, kai mes tai įgyvendiname, jis gali leisti mums nuskaityti 1024 prievadus per kelias minutes ir, be abejo, mažiau nei pusvalandį.
Mėginio išvestis:
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
Didesniems uostų diapazonams ir keliems pagrindiniams kompiuteriams labai gerai kyla daugiapakopis požiūris. „Semaforo“ modelis garantuoja, kad mums netrūksta sistemos išteklių, nepaisant nuskaitymo per tūkstantį uostų.
4. Paslaugų aptikimo pridėjimas
Dabar, kai turime greitą, efektyvų prievadų skaitytuvą, kitas žingsnis yra žinoti, kokios paslaugos veikia tuose atvirus prievadus. Tai paprastai žinoma kaip „aptarnavimo pirštų atspaudai“ arba „Banner Griebing“, procesas, kuriuo mes jungiame prie atvirų prievadų ir ištirsime grąžintus duomenis.
„Banner Gripbing“ įgyvendinimas
Bannerio griebimas yra tada, kai atidarome paslaugą ir perskaitome atsakymą (reklamjuostę), kurią ji mums siunčia. Taigi tai yra geras būdas nustatyti, ar kažkas vyksta, nes šiose reklamjuostėse yra daug paslaugų.
Dabar pridėkime reklamjuostę, patraukiančią mūsų skaitytuvą:
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)
}
}
Vykdymo paslaugų identifikavimas
Paslaugų aptikimui naudojame dvi pagrindines strategijas:
- Identifikavimas prie uosto: Žemėlapyje su paprastais prievado numeriais (pvz., 80 prievadas yra HTTP), mes tikėtina, kad spėjame paslaugai.
- Reklaminės juostos analizė: Mes imamės reklamjuostės teksto ir ieškome paslaugų identifikatorių ir informacijos apie versiją.
Pirmoji funkcija „Grabbanner“ bando patraukti pirmąjį atsakymą iš paslaugos. Kai kurios paslaugos, tokios kaip HTTP, reikalauja, kad mes atsiųstume užklausą ir gautume atsakymą, kuriam mes naudojame pridėti konkrečius atvejus.
Pagrindinė versijos aptikimas
Versijos aptikimas yra svarbus norint nustatyti pažeidžiamumus. Jei įmanoma, mūsų skaitytuvas parodo paslaugų reklamjuostes, kad patrauktų versijos informaciją:
- Ssh: Paprastai teikia informaciją apie versiją „SSH-2. 0-OpenSSH_7.4“ forma
- HTTP serveriai: Paprastai atsakykite į jų versijos informaciją atsakymo antraštėse, tokiose kaip „Serveris: Apache/2.4.29“
- Duomenų bazių serveriai: Gali atskleisti informaciją apie versiją savo pasveikinimo pranešimuose
Dabar išvestis grąžina daug daugiau informacijos apie kiekvieną atvirą prievadą:
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...
Ši patobulinta informacija yra daug vertingesnė pažeidžiamumo vertinimui.
5. Pažeidžiamumo aptikimo įgyvendinimas
Dabar, kai galime išvardyti veikiančias paslaugas ir kokia versija jos yra, mes ketiname įgyvendinti pažeidžiamumų aptikimą. Informacija apie paslaugas bus išanalizuota ir palyginta su žinomais pažeidžiamumais.
Rašyti paprastus pažeidžiamumo testus
Mes suformuosime duomenų bazę iš žinomų pažeidžiamumų, pagrįstų įprastomis paslaugomis ir versijomis. Paprastumo dėlei mes sukursime kodo pažeidžiamumo duomenų bazę, nors ir realaus pasaulio scenarijuje skaitytuvas greičiausiai užklausos išorinių pažeidžiamumo duomenų bazių (tokių kaip CVE ar NVD).
Dabar užauginkime savo kodą, kad aptiktume pažeidžiamumus:
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)
}
}
}
}
Verslas pagrįstas pažeidžiamumų atitikimas
Mes turime naivų pažeidžiamumo aptikimo versijų derinimo metodą:
- Tiesioginis atitikimas: Čia mes suderiname paslaugų tipą ir versiją su savo pažeidžiamumo duomenų baze.
- Dalinis atitikimas: Siekdami pažeidžiamos versijos suderinimo, mes atliekame versijos eilutės izoliavimo patikrinimus, leidžiančius mums nustatyti pažeidžiamas sistemas, net jei versijos eilutėje yra papildomos informacijos.
Realiame skaitytuve šis suderinimas būtų sudėtingesnis, atsižvelgiant į:
- Versijos diapazonai (ty paveikti 2.4.0–2.4.38 versijos)
- Konfigūracijai būdingi pažeidžiamumai
- OS specifiniai klausimai
- Daugiau niuansų versijų palyginimai
Pranešti apie tai, ką randame
Rezultatų ataskaita yra paskutinis pažeidžiamumo aptikimo žingsnis ir tai turi būti padaryta glaustai ir veiksmingu formatu. Mūsų skaitytuvas dabar:
- Išvardija visus atidarytus prievadus su aptarnavimo ir versijos informacija
- Kiekvienai pažeidžiamai paslaugai rodomi:
- Pažeidžiamumo ID (pvz., CVE numeris)
- Pažeidžiamumo aprašymas
- Sunkumo įvertinimas
- Nuorodos nuoroda Norėdami gauti daugiau informacijos
Mėginio išvestis:
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
Šis kruopštus pažeidžiamumo duomenų kibernetinio saugumo specialistus nurodo, kad būtų galima greitai nustatyti ir suskirstyti saugumo problemas, kurioms reikia išspręsti.
Galutiniai prisilietimai ir naudojimas
Dabar turite pagrindinį pažeidžiamumo skaitytuvą su paslaugų aptikimu ir pažeidžiamumo atitikimu; Dabar šiek tiek nušlimėkime, kad būtų praktiškiau naudoti realiame pasaulyje.
Komandos eilutės argumentai
Mūsų skaitytuvas turėtų būti konfigūruojamas per komandų eilutės vėliavas, kurios gali nustatyti taikinius, prievadų diapazonus ir nuskaitymo parinktis. Tai paprasta su „Go“ vėliavos paketu.
Toliau pridėkime komandų eilutės argumentus:
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
}
Išvesties formatavimas
Mūsų skaitytuvas dabar gali išvesti į tris formatus:
- Tekstas: Lengva skaitoma, lengva rašyti, puikiai tinka interaktyviam naudojimui.
- JSON: Struktūruotas išvestis, naudinga mašinų apdorojimui ir integracijai su kitais įrankiais.
- CSV: Skaičiuoklis suderinamas analizės ir ataskaitų teikimo formatas.
Išvesties tekste taip pat pateikiama daugiau informacijos, pvz., Neapdorotos reklamjuostės informaciją, jei nustatyta žodžių vėliava. Tai taip pat yra patogu derinant ar giliai nardymo analizę.
Naudojimo ir rezultatų pavyzdys
Taigi, čia yra keletas galimybių, jei ketinate naudoti mūsų skaitytuvą skirtingoms progoms:
Pagrindinis vieno kompiuterio nuskaitymas:
$ go run main.go -host example.com
Nuskaitykite konkretų uosto diapazoną:
$ go run main.go -host example.com -start 80 -end 443
Išsaugokite JSON failo rezultatus:
$ go run main.go -host example.com -format json -output results.json
Pažymėtinis nuskaitymas padidėjus laiko tarpiniam laikui:
$ go run main.go -host example.com -verbose -timeout 2000
Nuskaitykite su didesne suderinamumu, kad būtų greitesni rezultatai:
$ go run main.go -host example.com -concurrency 200
Teksto išvesties pavyzdys:
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
JSON išvesties pavyzdys:
{
"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": ()
}
)
}
Mes sukūrėme tvirtą tinklo pažeidžiamumo skaitytuvą „Go“, kuris parodo kalbos tinkamumą saugos priemonėms. Mūsų skaitytuvas greitai atveria prievadus, nustato juose veikiančias paslaugas ir nustato, ar yra žinomų pažeidžiamumų, ar ne.
Jis siūlo naudingos informacijos apie tinkle veikiančias paslaugas, įskaitant daugiapakopį, aptarnavimo pirštų atspaudus ir įvairius išvesties formatus.
Atminkite, kad tokios priemonės kaip skaitytuvas turėtų būti naudojamas tik pagal etinius ir teisinius parametrus, turint tinkamą leidimą nuskaityti tikslines sistemas. Atliekant atsakingai, reguliarus pažeidžiamumo skenavimas yra kritinis geros saugumo laikysenos aspektas, kuris gali padėti apsaugoti jūsų sistemas nuo grasinimų.
Galite rasti visą šio projekto šaltinio kodą „GitHub“