譯者 | 劉濤
審校 | 重樓
滲透測試能夠幫助組織發現網絡中潛在的安全漏洞,明確了在這些漏洞被惡意行為者利用之前開展修復工作的必要性。
在本文中,我們將使用Go語言創建一個簡易卻頗具健壯性的網絡漏洞掃描程序。Go是一種非常適合網絡編程的語言,這是因為其在設計之初就充分考量了并發性,而且擁有一套出色的標準庫。
創建項目
創建漏洞掃描程序
首先,我們用Go語言編寫一個簡易的命令行界面(CLI)工具,該工具能夠對主機網絡進行掃描,查找開放端口、正在運行的服務,并發現潛在的漏洞。這個掃描工具啟動方式簡易,并且隨著我們逐步為其增添更多功能模塊,它的功能將愈發強大。
以下建立一個新的Go工程:
mkdir goscan
cd goscan
go mod init github.com/yourusername/goscan
這一步操作會為項目初始化一個全新的Go模塊。該模塊能夠幫助我們對項目所需的依賴項進行有效管理。
配置包與環境
針對我們的掃描程序,我們將使用幾個Go包:
package main
import (
"fmt"
"net"
"os"
"strconv"
"sync"
"time"
)
func main() {
fmt.Println("GoScan Network Vulnerability Scanner")
}
以上僅僅是初始設置。就一些初步功能而言,這些設置已然足夠。不過,后續我們會依據實際需求添加更多導入內容。目前,諸如“net”這類Go內置的其他標準庫包將承擔起我們所需的大部分網絡相關工作,而“sync”包則會負責處理并發操作等等。
網絡掃描的倫理考量與風險
在開始實施網絡掃描之前,有必要深入探討其中涉及的倫理問題。在全球眾多地區,未經授權開展網絡掃描或進行網絡枚舉屬于違法行為,這類行為還會被視為網絡攻擊的一種形式。因此,在進行網絡掃描時,務必始終遵循以下規則:
- 權限獲取:僅對自己有權限訪問的網絡和系統進行掃描,或者是在獲得明確的掃描授權之后開展操作。
- 掃描范圍界定:為掃描工作設定清晰明確的范圍,并嚴格將其限定在該范圍內。
- 掃描時機選擇:要避免過度掃描,防止因頻繁掃描導致目標系統服務中斷,或者觸發不必要的安全警報。
- 漏洞披露原則:一旦發現漏洞,應以負責任的態度將相關情況報告給對應的系統所有者。
- 法律合規遵循:充分了解并嚴格遵守當地有關網絡掃描的法律法規。
需要明確的是,濫用掃描工具可能會引發一系列嚴重后果,包括但不限于面臨法律訴訟、造成目標系統損壞,甚至導致意外的拒絕服務情況。盡管我們開發的掃描程序會設置諸如速率限制等防護機制,但最終以符合倫理道德和法律規范的方式使用該工具,仍是每位使用者應盡的責任。
簡易端口掃描程序
漏洞評估的基礎在于端口掃描。每個開放端口所提供的、可能存在易受攻擊情況的服務信息,就是我們要探尋的關鍵內容。接下來,讓我們用Go語言編寫一個簡易的端口掃描程序。
端口掃描的底層實現
端口掃描的原理是嘗試與目標主機上的每個可能端口建立連接。若連接成功,這就意味著該端口處于開放狀態;若連接失敗,則表明該端口處于關閉狀態或者被設置了過濾規則。Go語言的“net”包為實現這一功能提供了必要的支持。
以下是我們實現的一個簡易端口掃描程序:
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包,該包提供了豐富的網絡輸入/輸出接口(I/O interfaces)以及相關函數。具體而言,主要涉及以下幾個關鍵部分:
- net.DialTimeout函數:此函數的功能是嘗試在預先設定的超時時間內,與指定的TCP網絡地址建立連接。在連接過程中,如果操作成功,它將返回一個連接對象;若出現連接錯誤,函數則會返回相應的錯誤信息。
- 連接處理機制:當連接成功建立時,也就意味著對應的端口處于開放狀態。為了避免資源的浪費和不必要的占用,我們會在確認端口開放后,立即關閉該連接,以釋放相關資源,確保系統的高效運行。
- 超時參數設定:我們為連接操作指定了一個超時時間。這一設置的目的在于防止程序在遇到被過濾的開放端口時,出現長時間等待(掛起)的情況。通常來說,將初始超時時間設置為2秒是一個較為合適的選擇。不過,實際應用中可根據具體的網絡狀況和需求進行靈活調整。
測試我們的首次掃描
接下來,讓我們針對本地主機運行這個簡易掃描程序。在本地主機上,通常會運行著一些服務。具體操作步驟如下:
- 將代碼保存為名為 main.go 的文件
- 使用 go run main.go 命令運行該文件執行上述操作后,程序將顯示哪些本地端口處于開放狀態。在一般的開發計算機上,依據你所運行的服務不同,可能會出現 80 端口(用于 HTTP 服務)、443 端口(用于 HTTPS 服務),或者其他正在使用的數據庫端口等。
以下是你可能得到的一些輸出結果:
Scanning host: localhost
Port 22 is open
Port 80 is open
Port 443 is open
Scan complete
使用該簡單掃描程序,盡管能夠實現基本的端口掃描功能,但也存在一些較為嚴重的不足:
- 掃描速度問題:該掃描程序按照順序逐個掃描端口,這種方式導致掃描速度極為緩慢。在面對大量端口需要掃描的場景時,會耗費大量時間。
- 信息獲取不足:它僅僅能夠告知我們端口處于開放還是關閉狀態,并沒有提供任何與端口所對應服務相關的信息。這使得我們難以進一步了解目標主機的詳細情況。
- 掃描范圍受限:目前我們僅對前 1024 個端口進行掃描。然而,在實際的網絡環境中,許多服務可能會使用 1024 以上的端口,這就導致該掃描程序無法全面檢測到所有可能存在的開放端口及相關服務。
鑒于上述這些限制因素,我們的掃描程序在實際應用場景中的實用性大打折扣。
從這里開始改進:多線程掃描
為何第一版速度慢
我們的第一個端口掃描程序雖能運行,但速度慢得讓人難以接受。問題在于它使用順序掃描的方式——一次僅能探測一個端口。當主機有大量關閉或過濾的端口時,在轉向下一個端口之前,我們會浪費時間等待每個端口的連接超時。
為了讓你了解這個問題,我們來看一下簡易掃描程序的用時情況:
- 掃描前1024個端口,在最壞的情況下,若每個端口超時時間設為2秒,最多需要2048秒(超過34分鐘)。
- 但即使到關閉端口的連接能立即失敗,由于網絡延遲,這種方法效率依然低下。
這種逐個掃描的方式是任何真正的漏洞掃描工具的瓶頸。
添加多線程支持
Go語言在使用goroutine(協程)和channel(通道)做并發處理方面表現尤為出色。因此,我們利用這些功能嘗試一次同時掃描多個端口,這將顯著提高性能。
現在,讓我們創建一個多線程端口掃描程序:
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)
}
}
多線程結果
現在,讓我們一同審視改進后的掃描程序在性能提升以及并發機制運行方面的具體情況:
- 協程(Goroutines):為有效提升掃描效率,針對每個待掃描的端口,我們都會啟動一個獨立的協程。通過這種方式,系統能夠在檢查某一個端口狀態的同時,并行地對其他端口展開檢查,極大地提高了掃描的并行度。
- 等待組(WaitGroup):隨著協程的大量引入,我們需要一種機制來確保所有協程都能順利完成任務。“sync.WaitGroup”在此發揮了關鍵作用,它能夠幫助我們精準跟蹤所有正在運行的協程,并使程序在所有協程執行完畢之后再繼續后續操作,保證了掃描任務的完整性和準確性。
- 結果通道(Result Channel):我們專門創建了一個帶有緩沖的通道,其作用是按順序接收所有協程返回的掃描結果。這種設計使得掃描結果能夠有序地進行收集和處理,方便我們對掃描結果進行統一管理和分析。
- 信號量模式(Semaphore Pattern):借助通道,我們成功實現了信號量模式。通過這一模式,我們能夠對允許并行進行的掃描數量進行有效限制。這一舉措至關重要,它可以避免因同時打開過多連接,給目標系統甚至自身運行的機器帶來過大的負載壓力,確保掃描過程的穩定性和安全性。
- 減少超時(Reduced Timeout):鑒于我們使用并行方式運行大量的端口掃描任務,為了提高整體掃描效率并合理利用資源,我們使用相對較短的超時。
通過上述一系列改進措施,性能差距對比表現得十分明顯。在實際應用中,當我們成功實現此方法后,掃描1024個端口所需的時間大幅縮短,僅僅只需幾分鐘,肯定不會超過半小時。
以下為示例輸出:
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
多線程方法具備出色的擴展性,能夠很好地適用于更大的端口掃描范圍以及多個主機的掃描任務。其中,信號量模式發揮了關鍵作用,它確保即便掃描的端口數量超過1000個,系統資源也不會被耗盡。
添加服務檢測
既然我們已經構建了一個快速且高效的端口掃描程序,接下來的關鍵步驟便是弄清楚在那些處于開放狀態的端口上究竟運行著何種服務。這一過程在業內通常被稱作 “服務指紋識別(service fingerprinting)” 或者 “橫幅抓取(banner grabbing)”,簡單來講,就是連接到開放端口,并對返回的數據展開檢測。
橫幅抓取的實現
所謂橫幅抓取,指的是我們與一個服務建立連接后,讀取該服務發送給我們的響應信息(即橫幅)。這是一種非常有效的識別服務是否正在運行的方法,因為許多服務會在這些橫幅信息中明確表明自身的身份。
接下來,讓我們在掃描程序中增添橫幅抓取功能:
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)
識別正在運行的服務
我們主要使用兩種策略來檢測正在運行的服務:
- 基于端口的識別:通過將常見的端口號進行映射(例如,80端口通常對應 HTTP服務),我們能夠對正在運行的服務做出合理的推測。
- 橫幅分析:我們對獲取到的橫幅文本進行提取操作,從中查找能夠標識服務的標識符以及版本信息。
其中,第一個函數“grabBanner”的作用是嘗試從服務端獲取首個響應。對于某些特定的服務,例如 HTTP 服務,由于我們需要先發送請求然后才能接收回復,針對此類情況,我們會添加專門的處理邏輯或測試用例。
基礎版本檢測
版本檢測在識別潛在漏洞的過程中具有重要意義。只要條件允許,我們的掃描程序就會對服務橫幅進行解析,以此來提取其中的版本信息:
- SSH服務:其版本信息通常以 “SSH-2.0-OpenSSH_7.4” 這樣的格式呈現。
- HTTP服務器:一般會在響應頭部返回版本信息,例如 “Server: Apache/2.4.29”。
- 數據庫服務器:可能會在歡迎消息中透露出版本相關信息。
經過上述功能添加后,現在對于每個開放端口,掃描程序的輸出將會返回更為豐富的信息:
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...
這些經過增強的信息,對于開展漏洞評估而言,具備更高的價值。
漏洞檢測實施
既然我們已經能夠枚舉正在運行的服務及其版本,接下來就需要實現漏洞檢測功能。我們將對獲取到的服務信息進行深入分析,并與已知的漏洞信息進行比對。
編寫簡單的漏洞測試
我們將基于常見服務及其版本信息,構建一個已知漏洞的數據庫。為了簡化操作流程,我們創建一個代碼內漏洞數據庫。不過在實際應用場景中,掃描程序通常會查詢外部的專業漏洞數據庫,比如 CVE(通用漏洞披露)或 NVD(國家漏洞數據庫)。
下面,讓我們對現有代碼進行擴展,以實現漏洞檢測功能:
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)
}
}
}
}
基于版本的漏洞匹配
我們使用一種相對簡單的基于版本匹配的漏洞檢測方式:
- 直接匹配:在這里,我們會將檢測到的服務類型和版本信息,直接與漏洞數據庫中的記錄進行匹配。若匹配成功,則可確定存在相應漏洞。
- 部分匹配:針對易受攻擊的版本匹配情況,我們會對版本字符串執行包含性驗證(containment checks)。這種方式具有較高的靈活性,即便版本字符串中包含一些額外信息,也能夠準確識別出存在漏洞的系統。
需要注意的是,在實際掃描過程中,這種匹配過程會更加復雜,需要綜合考慮以下多種因素: - 版本范圍:例如明確指出某個軟件版本號在2.4.0至2.4.38區間內存在受影響的情況。
- 特定配置的漏洞:不同的系統配置可能會導致特定的漏洞出現,這部分因素也需要納入考量。
- 特定操作系統的問題:某些漏洞可能是特定操作系統環境下才會出現的,掃描時需要結合操作系統信息進行判斷。
- 更細致的版本比較:除了簡單的版本號比對,還需要進行更精細的版本比較邏輯,以確保漏洞判斷的準確性。
報告檢測結果
報告檢測結果是整個漏洞檢測流程的最后一步,需要以簡潔且可操作執行的格式呈現出來。目前,我們的掃描程序具備以下功能:
- 詳細列出所有開放端口,并附帶相應的服務名稱以及版本信息。
- 針對每個檢測出存在漏洞的服務,展示以下關鍵信息:
A.漏洞 ID:例如常見的 CVE 編號,用于唯一標識該漏洞。
B.漏洞描述:清晰闡述該漏洞的具體情況和影響。
C.嚴重程度評級:對漏洞的嚴重程度進行評估和分級,方便用戶快速了解風險程度。
D.更多相關信息的參考鏈接:提供指向更多關于該漏洞詳細信息的參考鏈接,以便用戶進一步深入了解和采取應對措施。
以下為示例輸出:
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
這些全面的漏洞數據能夠為網絡安全專家提供有力支持,幫助他們及時排查出需要處理的安全問題,并依據相關情況對這些問題進行優先級排序。
最后的優化與使用方法
目前,你已經擁有了一個基礎的漏洞掃描程序,它具備服務檢測和漏洞匹配功能。接下來,我們將對其進行一些優化,以提升該掃描程序在實際應用中的實用性。
命令行參數
我們希望掃描程序能夠通過命令行標志進行靈活配置,以此來設置掃描目標、端口范圍以及其他掃描選項。借助Go語言的“flag”包,實現這一功能并非難事。
下面,我們開始添加命令行參數:
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
}
輸出格式
當前,我們的掃描程序支持三種輸出格式,以滿足不同用戶在不同場景下的多樣化需求:
- 文本格式:這種格式具有易讀易寫的特點,極大地方便了用戶在交互式環境中的使用體驗。用戶可以直觀地查看掃描結果,快速獲取關鍵信息。
- JSON格式:該格式提供了結構化的輸出方式,非常有利于機器進行處理。同時,它也便于與其他各類工具進行集成,能夠實現數據在不同系統間的高效流轉與共享。
- CSV格式:作為一種與電子表格兼容的格式,CSV 格式特別適用于對掃描結果進行分析和生成報告。用戶可以方便地將數據導入到電子表格軟件中,進行進一步的數據處理和可視化操作。
此外,如果用戶設置了詳細模式標志,掃描程序輸出的文本內容將包含更多詳細信息,例如原始橫幅信息。這些額外信息對于調試掃描過程中的問題或進行深入的安全分析都具有重要的輔助作用。
示例用法與結果
接下來,為你展示在不同場景下使用我們掃描程序時,可能出現的一些情況:
對單個主機進行基礎掃描:
$ go run main.go -host example.com
掃描特定端口范圍:
$ go run main.go -host example.com -start 80 -end 443
將結果保存到JSON文件里:
$ go run main.go -host example.com -format json -output results.json
增加超時的詳細掃描:
$ go run main.go -host example.com -verbose -timeout 2000
以更高的并發性進行掃描以獲得更快的結果:
$ 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語言成功構建了一個功能強大的網絡漏洞掃描程序,這一成果充分彰顯了Go語言在開發安全工具領域的卓越適用性。此掃描程序具備諸多出色能力,能夠迅速掃描開放端口,精準識別端口上正在運行的服務,并準確判斷是否存在已知漏洞。
該掃描程序不僅能夠提供有關網絡中運行服務的實用信息,還集成了多線程處理機制、先進的服務指紋識別功能,并且支持多種輸出格式,以滿足不同用戶在多樣化場景下的需求。
在此特別提醒,類似這樣的掃描工具,務必在嚴格遵循道德和法律規范的前提下使用,并且必須事先獲取對掃描目標系統的適當授權。當以負責任的態度開展操作時,定期進行漏洞掃描是維持良好安全態勢的關鍵因素,能夠切實幫助保護你的系統免受潛在威脅的侵害。
如果你希望進一步了解該項目,可在GitHub上找到此項目的完整源代碼。
譯者介紹
劉濤,51CTO社區編輯,某大型央企系統上線檢測管控負責人。
原文標題:Building a Network Vulnerability Scanner with Go,作者:Rez Moss