基于Golang構(gòu)建一個(gè)網(wǎng)絡(luò)漏洞掃描器
本文將用 Go 語言創(chuàng)建一個(gè)簡單且相當(dāng)可靠的網(wǎng)絡(luò)漏洞掃描器。Go 語言非常適合網(wǎng)絡(luò)編程,它在設(shè)計(jì)時(shí)就考慮到了并發(fā)性,并且擁有出色的標(biāo)準(zhǔn)庫。

1. 項(xiàng)目設(shè)置
創(chuàng)建漏洞掃描器
我們想要開發(fā)一個(gè)簡單的命令行工具,該工具能夠掃描主機(jī)網(wǎng)絡(luò)、查找開放端口、識(shí)別運(yùn)行的服務(wù)并發(fā)現(xiàn)潛在漏洞。這個(gè)掃描器一開始會(huì)非常簡單,但隨著逐步添加功能,其能力會(huì)不斷增強(qiáng)。
首先,我們將創(chuàng)建一個(gè)新的 Go 項(xiàng)目:
mkdir goscan cd goscan go mod init github.com/yourusername/goscan
這將為項(xiàng)目初始化新的Go模塊,幫助我們管理依賴項(xiàng)。
配置包和環(huán)境
掃描器將利用若干 Go 包:
package main
import (
"fmt"
"net"
"os"
"strconv"
"sync"
"time"
)
func main() {
fmt.Println("GoScan - Network Vulnerability Scanner")
}
這只是初始設(shè)置,但對(duì)于初始功能來說,已經(jīng)足夠了,我們會(huì)根據(jù)需要添加更多導(dǎo)入內(nèi)容。像 net 這樣的標(biāo)準(zhǔn)庫包將負(fù)責(zé)處理大部分網(wǎng)絡(luò)相關(guān)操作,而 sync 則會(huì)負(fù)責(zé)并發(fā)處理等。
網(wǎng)絡(luò)掃描的倫理考量與風(fēng)險(xiǎn)
在開始實(shí)現(xiàn)之前,首先需要探討一下與網(wǎng)絡(luò)掃描相關(guān)的倫理問題。在許多地區(qū),未經(jīng)授權(quán)的網(wǎng)絡(luò)掃描是違法的,會(huì)被視為發(fā)動(dòng)網(wǎng)絡(luò)攻擊的一種手段。因此,必須始終遵守以下規(guī)則:
- 許可:僅對(duì)擁有所有權(quán)或已獲得明確許可進(jìn)行掃描的臨時(shí)網(wǎng)絡(luò)和系統(tǒng)進(jìn)行掃描。
- 范圍:為掃描設(shè)定明確的范圍,并不要超出該范圍。
- 時(shí)間:不要進(jìn)行可能導(dǎo)致服務(wù)中斷或引發(fā)安全警報(bào)的過度掃描。
- 披露:如果發(fā)現(xiàn)漏洞,請(qǐng)負(fù)責(zé)任的將其報(bào)告給相應(yīng)的系統(tǒng)所有者。
- 法律合規(guī):了解并遵守有關(guān)網(wǎng)絡(luò)掃描的當(dāng)?shù)胤伞?/li>
掃描工具的不當(dāng)使用可能會(huì)導(dǎo)致法律訴訟、系統(tǒng)損壞或意外服務(wù)中斷。我們的掃描器將包含諸如速率限制等防護(hù)措施,但最終責(zé)任在于用戶以合乎道德的方式使用。
2. 簡單端口掃描器
漏洞評(píng)估基于端口掃描。每個(gè)開放端口所提供的潛在易受攻擊的服務(wù)信息正是我們所要查找的內(nèi)容?,F(xiàn)在,讓我們用 Go 語言編寫一個(gè)簡單的端口掃描器。
低級(jí)端口掃描實(shí)現(xiàn)
端口掃描:嘗試與目標(biāo)主機(jī)上的每一個(gè)可能的端口建立連接。如果連接成功,則該端口是開放的;如果連接失敗,則該端口是關(guān)閉的或被過濾的。對(duì)于此功能,Go 的 net 包已經(jīng)為我們提供了支持。
那么,這就是我們所設(shè)計(jì)的一種簡單的端口掃描器的示例:
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" // Change this to your target
timeout := time.Second * 2
fmt.Printf("Scanning host: %s\n", host)
// Scan ports 1-1024 (well-known ports)
for port := 1; port <= 1024; port++ {
if scanPort(host, port, timeout) {
fmt.Printf("Port %d is open\n", port)
}
}
fmt.Println("Scan complete")
}
使用net包
上述代碼使用了 Go 語言的 net 包,該包提供了網(wǎng)絡(luò)輸入/輸出接口及相關(guān)函數(shù)。那么,主要的組成部分都有哪些呢?
- net.DialTimeout:此功能會(huì)嘗試在設(shè)定的超時(shí)時(shí)間內(nèi)連接到 TCP 網(wǎng)絡(luò)地址。如果連接成功,會(huì)返回連接信息以及任何可能出現(xiàn)的錯(cuò)誤。
- 連接處理:如果連接過程順利,我們便知道該連接已打開,并會(huì)立即關(guān)閉該連接以釋放資源。
- 超時(shí)參數(shù):設(shè)定超時(shí)時(shí)間,以避免在任何被過濾的開放端口上陷入僵局。兩秒是一個(gè)不錯(cuò)的初始值,但可根據(jù)網(wǎng)絡(luò)狀況進(jìn)行調(diào)整。
對(duì)首次掃描進(jìn)行測試
現(xiàn)在,我們?cè)诒镜刂鳈C(jī)上運(yùn)行簡單掃描器,那里可能有一些服務(wù)正在運(yùn)行。
- 將代碼保存到名為
main.go的文件中 - 用
go run main.go命令運(yùn)行
這將顯示哪些本地端口是開放的。在普通開發(fā)機(jī)器上,可能會(huì)有 80(HTTP)端口、443(HTTPS)端口,或者根據(jù)運(yùn)行的服務(wù)不同,還有其他任意數(shù)量的數(shù)據(jù)庫端口正在使用。
以下是一些可能得到的示例輸出:
Scanning host: localhost
Port 22 is open
Port 80 is open
Port 443 is open
Scan complete
使用這種基本的掃描器是可以的,但也有不少明顯的缺點(diǎn):
- 速度:由于是按順序掃描端口,所以速度極其緩慢。
- 信息:只是告訴我們某個(gè)端口是否開放,沒有服務(wù)信息。
- 覆蓋范圍有限:只掃描前 1024 個(gè)端口。
這些限制使得難以在實(shí)際應(yīng)用中使用掃描器。
3. 從這里開始改進(jìn):多線程掃描
為何最初的版本運(yùn)行緩慢
第一個(gè)端口掃描器能夠正常工作,但其運(yùn)行速度極其緩慢,幾乎無法實(shí)際使用。問題在于其采用順序掃描方法 —— 一次掃描一個(gè)端口。當(dāng)一臺(tái)主機(jī)有很多關(guān)閉/過濾的端口時(shí),會(huì)在每個(gè)端口上等待連接超時(shí),然后再轉(zhuǎn)移到下一個(gè)端口,這造成了極大的時(shí)間浪費(fèi)。
為了展示這個(gè)問題,我們來看看基本掃描器的運(yùn)行時(shí)間:
- 對(duì)于掃描前 1024 個(gè)端口的情況,如果設(shè)置 2 秒的超時(shí)時(shí)間,最長時(shí)間將達(dá) 2048 秒(超過 34 分鐘)。
- 但即便對(duì)那些已關(guān)閉的端口的連接也會(huì)立即失敗,這種方法由于網(wǎng)絡(luò)延遲的原因也是效率低下。
這種逐個(gè)端口進(jìn)行的掃描方式是任何真正的漏洞掃描工具的瓶頸。
添加線程支持
Go 語言在利用協(xié)程和通道實(shí)現(xiàn)并發(fā)方面表現(xiàn)尤為出色。因此,我們利用這些特性嘗試同時(shí)掃描多個(gè)端口,從而顯著提高性能。
現(xiàn)在讓我們來創(chuàng)建多線程端口掃描器:
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
// Create a buffered channel to collect results
resultChan := make(chan Result, end-start+1)
// Create a semaphore to limit concurrent goroutines
// This prevents us from opening too many connections at once
semaphore := make(chan struct{}, 100) // Limit to 100 concurrent scans
// Launch goroutines for each port
for port := start; port <= end; port++ {
wg.Add(1)
go func(p int) {
defer wg.Done()
// Acquire semaphore
semaphore <- struct{}{}
defer func() { <-semaphore }() // Release semaphore
result := scanPort(host, p, timeout)
resultChan <- result
}(port)
}
// Close channel when all goroutines complete
go func() {
wg.Wait()
close(resultChan)
}()
// Collect results from channel
for result := range resultChan {
if result.State {
results = append(results, result)
}
}
return results
}
func main() {
host := "localhost" // Change this to your target
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)
}
}
多線程結(jié)果
現(xiàn)在,我們來看看改進(jìn)后掃描器的性能提升以及并發(fā)機(jī)制:
- 協(xié)程:為了使掃描過程高效,我們會(huì)為需要掃描的每個(gè)端口啟動(dòng)一個(gè)協(xié)程,這樣當(dāng)我們檢查一個(gè)端口時(shí),就可以同時(shí)檢查其他端口。
- 等待組:同步的等待組 當(dāng)我們啟動(dòng)協(xié)程時(shí),希望等待它們完成。等待組有助于跟蹤所有正在運(yùn)行的協(xié)程,并等待它們完成。
- 結(jié)果通道:我們?yōu)樗袇f(xié)程的結(jié)果創(chuàng)建了一個(gè)緩沖通道。
- 信號(hào)量模式:使用信號(hào)量來限制并行進(jìn)行的掃描數(shù)量,通過通道來實(shí)現(xiàn),防止我們因打開過多連接而使目標(biāo)系統(tǒng)甚至自身機(jī)器不堪重負(fù)。
- 縮短超時(shí)時(shí)間:由于以并行方式運(yùn)行許多此類掃描,所以使用較短的超時(shí)時(shí)間。
性能差距很大。因此,當(dāng)我們實(shí)現(xiàn)這個(gè)功能時(shí),可以在幾分鐘內(nèi)掃描 1024 個(gè)端口,而且肯定不會(huì)超過半小時(shí)。
示例輸出:
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
這種多線程方法對(duì)于較大的端口范圍和多個(gè)主機(jī)來說具有極好的擴(kuò)展性。信號(hào)量模式確保即便要掃描上千個(gè)端口,也不會(huì)耗盡系統(tǒng)資源。
4. 添加服務(wù)檢測
既然已經(jīng)有了一個(gè)快速、高效的端口掃描器,接下來的步驟就是了解那些開放端口上運(yùn)行的是哪些服務(wù)。這通常被稱為“服務(wù)指紋識(shí)別”或“標(biāo)志抓取”,是一個(gè)連接到開放端口并檢查返回?cái)?shù)據(jù)的過程。
服務(wù)旗標(biāo)抓?。˙anner Grabbing)實(shí)現(xiàn)
服務(wù)旗標(biāo)抓取指的是當(dāng)我們打開服務(wù)并讀取發(fā)送給我們的響應(yīng)(即旗標(biāo)信息)時(shí)的操作。因此,這是一種很好的確認(rèn)服務(wù)是否運(yùn)行的方法,許多服務(wù)都會(huì)在旗標(biāo)中標(biāo)識(shí)自身信息。
我們?cè)趻呙杵髦屑尤胱ト∑鞓?biāo)的功能:
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))
// Some services need a trigger to send data
// Send a simple HTTP request for web services
if port == 80 || port == 443 || port == 8080 || port == 8443 {
fmt.Fprintf(conn, "HEAD / HTTP/1.0\r\n\r\n")
} else {
// For other services, just wait for the banner
// Some services may require specific triggers
}
// Read the response
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",
}
// Try to identify service from common ports
service := "Unknown"
if s, exists := commonPorts[port]; exists {
service = s
}
version := "Unknown"
lowerBanner := strings.ToLower(banner)
// SSH version detection
if strings.Contains(lowerBanner, "ssh") {
service = "SSH"
parts := strings.Split(banner, " ")
if len(parts) >= 2 {
version = parts[1]
}
}
// HTTP server detection
if strings.Contains(lowerBanner, "http") || strings.Contains(lowerBanner, "apache") ||
strings.Contains(lowerBanner, "nginx") {
if port == 443 {
service = "HTTPS"
} else {
service = "HTTP"
}
// Try to find server info in format "Server: Apache/2.4.29"
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)
}
}
識(shí)別正在運(yùn)行的服務(wù)
兩種主要的服務(wù)檢測策略:
- 基于端口識(shí)別:通過映射到公共端口號(hào)(例如,端口 80 是 HTTP),對(duì)服務(wù)有一個(gè)可能的猜測。
- 旗標(biāo)分析:獲取旗標(biāo)文本并查找服務(wù)標(biāo)識(shí)符和版本信息。
第一個(gè)函數(shù) grabBanner 旨在從服務(wù)中獲取第一個(gè)響應(yīng)。有些服務(wù)(如 HTTP)要求發(fā)送請(qǐng)求并接收回復(fù),為此我們會(huì)添加特定案例來處理這種情況。
基本版本檢測
版本檢測對(duì)于漏洞的識(shí)別至關(guān)重要。在可能的情況下,掃描器會(huì)解析服務(wù)旗標(biāo)以獲取版本信息:
- SSH:通常會(huì)以
SSH-2.0-OpenSSH_7.4這樣的形式提供版本信息。 - HTTP 服務(wù)器:通常會(huì)在響應(yīng)頭中(如
Server: Apache/2.4.29)返回其版本信息。 - 數(shù)據(jù)庫服務(wù)器:可能會(huì)在其歡迎消息中披露版本信息。
現(xiàn)在,對(duì)于每個(gè)開放端口,輸出都會(huì)返回更多信息:
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...
這種增強(qiáng)后的信息對(duì)于漏洞評(píng)估而言要具有更高的價(jià)值。
5. 漏洞檢測實(shí)現(xiàn)
既然能列出正在運(yùn)行的服務(wù)及其版本,接下來我們將實(shí)現(xiàn)針對(duì)漏洞的檢測。我們對(duì)服務(wù)信息進(jìn)行分析,并與已知漏洞進(jìn)行對(duì)比。
編寫簡單的漏洞測試
我們將根據(jù)常見服務(wù)和版本,基于已知漏洞構(gòu)建數(shù)據(jù)庫。為了簡便起見,我們將創(chuàng)建一個(gè)嵌入代碼的漏洞數(shù)據(jù)庫,但在實(shí)際場景中,掃描器很可能會(huì)查詢外部漏洞數(shù)據(jù)庫(如 CVE 或 NVD)。
現(xiàn)在進(jìn)一步完善代碼,使其能夠檢測出漏洞:
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",
},
},
// Add more known vulnerabilities here
}
// checkVulnerabilities checks if a service/version combination has known vulnerabilities
func checkVulnerabilities(service, version string) []Vulnerability {
var vulnerabilities []Vulnerability
for _, vuln := range vulnerabilityDB {
// Simple matching - in a real scanner, this would be more sophisticated
if vuln.Service == service && strings.Contains(version, vuln.Version) {
vulnerabilities = append(vulnerabilities, vuln.Vulnerability)
}
}
return vulnerabilities
}
// grabBanner attempts to read the banner from an open port
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",
},
},
// Add more known vulnerabilities here
}
// checkVulnerabilities checks if a service/version combination has known vulnerabilities
func checkVulnerabilities(service, version string) []Vulnerability {
var vulnerabilities []Vulnerability
for _, vuln := range vulnerabilityDB {
// Simple matching - in a real scanner, this would be more sophisticated
if vuln.Service == service && strings.Contains(version, vuln.Version) {
vulnerabilities = append(vulnerabilities, vuln.Vulnerability)
}
}
return vulnerabilities
}
// grabBanner attempts to read the banner from an open port
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)
}
}
}
}
基于版本的漏洞匹配
針對(duì)漏洞檢測我們用簡單版本匹配方法:
- 直接匹配:將服務(wù)類型和版本與漏洞數(shù)據(jù)庫進(jìn)行匹配。
- 部分匹配:對(duì)于漏洞版本的匹配,我們會(huì)對(duì)版本字符串進(jìn)行限制性檢查,這樣即使版本字符串包含額外信息,也能識(shí)別出存在漏洞的系統(tǒng)。
在實(shí)際的掃描器中,匹配會(huì)更為復(fù)雜,會(huì)考慮到以下因素:
- 版本范圍(即版本 2.4.0 至 2.4.38 受影響)
- 特定配置的漏洞
- 操作系統(tǒng)特定的問題
- 更細(xì)致的版本比較
報(bào)告發(fā)現(xiàn)的情況
報(bào)告結(jié)果是漏洞檢測流程中的最后一步,需要以簡潔且具有可操作性的格式進(jìn)行。掃描器現(xiàn)在:
列出所有開放端口及其服務(wù)及版本信息
對(duì)于每個(gè)存在漏洞的服務(wù),會(huì)顯示:
- 漏洞標(biāo)識(shí)(例如,CVE 編號(hào))
- 漏洞描述
- 嚴(yán)重程度評(píng)級(jí)
- 更多信息的參考鏈接
示例輸出:
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
這份詳盡的漏洞數(shù)據(jù)能夠幫助網(wǎng)絡(luò)安全專家迅速找出并排序出需要解決的安全問題。
最后完善與使用方法
現(xiàn)在已經(jīng)有了一個(gè)具備服務(wù)檢測和漏洞匹配功能的基本漏洞掃描器;我們對(duì)其進(jìn)行完善,以便在實(shí)際應(yīng)用中更具實(shí)用性。
命令行參數(shù)
掃描器可以通過命令行標(biāo)志進(jìn)行配置,這些標(biāo)志能夠設(shè)定目標(biāo)、端口范圍以及掃描選項(xiàng)。使用 Go 的 flag 包進(jìn)行配置非常簡單。
添加命令行參數(shù):
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
}{
// ... (same as before)
}
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
}
輸出格式
掃描器現(xiàn)在可以輸出三種格式:
- 文本:易于閱讀,易于編寫,非常適合交互使用。
- JSON:結(jié)構(gòu)化輸出,適用于機(jī)器處理以及與其他工具的集成。
- CSV:電子表格兼容的格式,用于分析和報(bào)告。
輸出文本還會(huì)提供更多信息,例如如果設(shè)置了詳細(xì)模式,則會(huì)提供原始旗標(biāo)信息,對(duì)于調(diào)試或深入分析也非常方便。
示例用法及結(jié)果
如果打算將掃描器用于不同場合,以下是一些可能的選擇:
單個(gè)主機(jī)的基本掃描:
$ go run main.go -host example.com
掃描指定端口范圍:
$ go run main.go -host example.com -start 80 -end 443
增加超時(shí)和詳細(xì)信息:
$ go run main.go -host example.com -verbose -timeout 2000
以更高的并發(fā)性掃描以獲得更快的結(jié)果:
$ go run main.go -host example.com -concurrency 200
示例文本輸出:
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 輸出示例:
{
"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": []
}
]
}
我們用 Go 語言構(gòu)建了一個(gè)強(qiáng)大的網(wǎng)絡(luò)漏洞掃描器,這表明該語言非常適合用于安全工具。掃描器能迅速打開端口,識(shí)別端口上運(yùn)行的服務(wù),并判斷是否存在已知漏洞。
掃描器提供了有關(guān)網(wǎng)絡(luò)上運(yùn)行的服務(wù)的有用信息,包括多線程、服務(wù)指紋識(shí)別以及多種輸出格式。
請(qǐng)記住,像掃描器這樣的工具只能在符合道德和法律規(guī)范的條件下使用,并且需要獲得對(duì)目標(biāo)系統(tǒng)的掃描授權(quán)。如果操作得當(dāng),定期進(jìn)行漏洞掃描是良好安全態(tài)勢的重要組成部分,能夠幫助保護(hù)系統(tǒng)免受威脅。可以在 GitHub 上找到該項(xiàng)目的完整源代碼。
到此這篇關(guān)于基于Golang構(gòu)建一個(gè)網(wǎng)絡(luò)漏洞掃描器的文章就介紹到這了,更多相關(guān)Go網(wǎng)絡(luò)漏洞掃描器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang接收post和get請(qǐng)求參數(shù)處理
本文主要介紹了golang接收post和get請(qǐng)求參數(shù)處理,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03
Go語言基礎(chǔ)之網(wǎng)絡(luò)編程全面教程示例
這篇文章主要為大家介紹了Go語言基礎(chǔ)之網(wǎng)絡(luò)編程全面教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
Golang?動(dòng)態(tài)腳本調(diào)研詳解
這篇文章主要為大家介紹了Golang?動(dòng)態(tài)腳本調(diào)研詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
go語言中使用ent做關(guān)聯(lián)查詢的示例詳解
go語言的ent框架是facebook開源的ORM框架,是go語言開發(fā)中的常用框架,而關(guān)聯(lián)查詢又是日常開發(fā)中的常見數(shù)據(jù)庫操作,故文本給出一個(gè)使用ent做關(guān)聯(lián)查詢的使用示例,需要的朋友可以參考下2024-02-02
go語言標(biāo)準(zhǔn)庫expvar監(jiān)控工具的實(shí)現(xiàn)
Go語言的expvar標(biāo)準(zhǔn)庫是專為暴露程序內(nèi)部指標(biāo)設(shè)計(jì)的輕量級(jí)監(jiān)控工具,支持通過HTTP接口以JSON格式實(shí)時(shí)查看運(yùn)行狀態(tài),本文就來詳細(xì)的介紹一下go語言標(biāo)準(zhǔn)庫expvar監(jiān)控工具的實(shí)現(xiàn),感興趣的可以了解一下2026-04-04
Golang 經(jīng)典校驗(yàn)庫 validator 用法解析
這篇文章主要為大家介紹了Golang 經(jīng)典校驗(yàn)庫 validator 用法解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08

