2025.4.20 周报 在这个项目中资产收集模块由我负责,目前主要实现的功能为:
IP 主机存活检测
端口扫描
指纹识别
扫描进程展示
存放数据库
接下来我将一步一步阐述我实现的每一个功能,和我从中学到的知识
一、IP 主机存活检测 通过 go-ping 库检测 原先我是想用 go 语言自带的 go-ping 库来实现通过 ping 去检测主机是否存活,但是很遗憾,出了几点问题:
要用 go-ping 库去发 ICMP 包需要管理员权限,Linux 下需要 root 权限 或 CAP_NET_RAW 能力但经过测试,我发现在 wsl 上用sudo运行也行不通,这就很奇怪了
用 go-ping 库的代码是 ai 写的,我看不懂(bushi
经过询问 ai 得到这样的结果
特性
系统 ping 命令
Go 程序 (go-ping)
实现方式
直接调用内核 ICMP 协议栈
使用原始套接字 (RAW socket)
权限要求
通常有 CAP_NET_RAW 能力
需要明确提权 (sudo/SetPrivileged)
数据包构造
内核自动构造合规 ICMP 包
程序手动构造 ICMP 包
防火墙穿透性
可能被特殊放行
更容易被拦截
还有云服务器的安全策略
许多云服务商(如阿里云/AWS):
允许标准 ICMP Echo (系统 ping)
拦截非常规 ICMP 请求 (原始套接字构造的包)
因为是 ai 回答,不知准确性,姑且这么认为。
代码实现(因为无法成功,仅仅只是记录)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
调动二进制命令ping检测 这里是直接用 go 来调用ping命令,完完全全可以成功
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func IsHostAlive (ip string ) bool { var cmd *exec.Cmd switch runtime.GOOS { case "windows" : cmd = exec.Command("ping" , "-n" , "2" , "-w" , "2000" , ip) default : cmd = exec.Command("ping" , "-c" , "2" , "-W" , "2" , ip) } output, err := cmd.CombinedOutput() if err != nil { log.Printf("ping %s failed: %v \noutput: %s" , ip, err, string (output)) return false } return true }
同时这里做了对不同系统的兼容,匹配了不同系统的 ping 命令语法。
同时,在这里有一个问题:
1 2 3 4 5 case "windows" : cmd = exec.Command("ping" , "-n" , "2" , "-w" , "2000" , ip)default : cmd = exec.Command("ping" , "-c" , "2" , "-W" , "2" , ip) }
这一部分的 ip由用户输入,是否存在安全问题,是否可以通过这个漏洞进行命令注入?
经过具体尝试,答案是否,实际上这里即使ip由用户控制,但仍然是安全的,原因是:
Go 的 exec.Command 不会通过 Shell 执行命令 ,而是直接调用目标程序(如 ping),并将参数作为字符串传递 ,而不是解析成 Shell 命令。
ping 收到的是 127.0.0.1|ifconfig 这个整体字符串 ,而不是 127.0.0.1 和 ifconfig 两个命令。
如果代码是 通过 Shell 执行 (如 bash -c "ping -c 2 $IP"),那么 |、&& 等符号会被解析,导致命令注入。
通过 TCP 连接检测主机是否存活 即使在上面调用二进制命令ping检测已经可行,但要是在某些时候禁用了 ICMP 协议,那就用不了ping命令,这种时候就要用备用方案——TCP 连接
代码如下
1 2 3 4 5 6 7 8 9 10 11 func IsHostAlive_TCP (ip string ) bool { port := []int {80 , 22 , 443 } for _, port := range port { conn, err := net.DialTimeout("tcp" , fmt.Sprintf("%s:%d" , ip, port), 3 *time.Second) if err == nil { conn.Close() return true } } return false }
不必多言(
二、端口扫描 原先在这个项目开始之前我是有写一个简易的扫描器的,但是那时我还没有学习 go 的并行设计,65535 个端口是一个循环一个一个扫的(太愚蠢了),并且当时还是拿127.0.0.1做的测试,扫的很快,我就以为没有什么大不了,等到我尝试去扫描我的服务器 IP 的时候我才知道这有多慢,一个小时仅仅只完成了百分之一……
后来就用一天时间去学习了 go 的并行设计,并主要处理了端口扫描这一部分,现在扫描的就比较迅速,扫描一次需要 48 秒。
我学习的主要是 go Worker 池并行设计,相比于普通 goroutine 的优势:
直接开 goroutine 的问题
Worker 池的优势
瞬间创建数万 goroutine 崩溃
控制并发数量(如500个)
任务分配不均
统一的任务队列,自动均衡负载
资源竞争严重
通过 Channel 实现线程安全通信
和原始代码相比:
原始代码
Worker池改进版
优势体现
单线程循环
多worker并发
速度提升数百倍
直接打印结果
统一结果收集处理
便于后续扩展(如存数据库)
硬编码参数
参数化配置
更灵活易用
无错误控制
内置重试机制
扫描更稳定
(为了方便展示,这里直接使用 ai 提供的表格)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 package mainimport ( "fmt" "log" "net" "runtime" "strconv" "sync" "time" )func ScanWorker (id int , ip string , scanner <-chan scanner, results chan <- scanResult, timeout time.Duration) { for scan := range scanner { result := scanResult{} result.address = net.JoinHostPort(ip, strconv.Itoa(scan.port)) if err := ValidateInput(scan.network, result.address); err != nil { log.Println(err) break } scan.conn, result.err = net.DialTimeout(scan.network, result.address, timeout) if result.err == nil { result.open = true result.banner = ReadBanner(scan.conn) result.service = IdentifyService(result.banner, scan.port) scan.conn.Close() } else { result.open = false result.errtype = ErrType(result) } results <- result } }func Run (ip string , network string ) []scanResult { var ( totalPorts = 65535 scanned = 0 startTime = time.Now() ) var wg sync.WaitGroup tasks := make (chan scanner, 1000 ) results := make (chan scanResult, 1000 ) var openPorts []scanResult workers := runtime.NumCPU() * 100 for i := 0 ; i < workers; i++ { wg.Add(1 ) go func (id int ) { defer wg.Done() ScanWorker(id, ip, tasks, results, 2 *time.Second) }(i) } go func () { for i := 1 ; i < 65536 ; i++ { tasks <- scanner{network: network, ip: ip, port: i, conn: nil } } close (tasks) }() go func () { wg.Wait() close (results) }() errCount := make (map [string ]int ) for result := range results { scanned++ if scanned%100 == 0 || scanned == totalPorts { printProgress(scanned, totalPorts, startTime) } if result.open { openPorts = append (openPorts, result) } else { if errCount[result.errtype] < 3 { fmt.Printf("[-] %s is %s\n" , result.address, result.errtype) } errCount[result.errtype]++ } } fmt.Println() printOpenPorts(openPorts) return openPorts }
这里是并行部分的函数设计,一些操作写在注释中了,不过多赘述。
三、指纹识别 我做到的指纹识别原理是(自我理解):先端口扫描,在端口扫描的时候创建了连接conn,而我需要做的就是通过这个连接获取到 banner 信息,并从 banner信息中提取我所需要的知晓的对应服务。如果从原先的conn中读取不到 banner 信息时,那就主动向服务器那边发送请求,再读取所响应的 banner 信息。
初代码 其实这里的实现是有些莫名其妙的,再这一周中我花了四天的时间去解决这一个问题,但始终没有解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
这里是我原先的代码,经过几天的修改可以识别 21端口的 ftp 服务和 22 端口的 ssh 服务,但是面对 http 的服务始终识别不到 banner 信息,经常遇到的错误信息就是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 GOROOT=D:\go GOPATH=C:\Users\13483\go GOPROXY=https://goproxy.cn,direct D:\go\bin\go.exe build -o C:\Users\13483\AppData\Local\JetBrains\GoLand2024.3\tmp\GoLand\___go_build_golandproject_scan.exe . C:\Users\13483\AppData\Local\JetBrains\GoLand2024.3\tmp\GoLand\___go_build_golandproject_scan.exe Please enter your ip:59.110.163.36 2025/04/18 21:33:00 read banner failed: read tcp 192.168.1.15:17878->59.110.163.36:888: i/o timeout [-] 59.110.163.36:12 is Timeout [-] 59.110.163.36:7 is Timeout [-] 59.110.163.36:222 is Timeout Scanning: 2800/65535 (4.3%) | Elapsed: 2s2025/04/18 21:33:01 read banner failed: read tcp 192.168.1.15:19329->59.110.163.36:80: i/o timeout Scanning: 22400/65535 (34.2%) | Elapsed: 16s2025/04/18 21:33:16 read banner failed: read tcp 192.168.1.15:41070->59.110.163.36:23333: i/o timeout Scanning: 30800/65535 (47.0%) | Elapsed: 22s2025/04/18 21:33:22 read banner failed: read tcp 192.168.1.15:48710->59.110.163.36:30957: i/o timeout Scanning: 65535/65535 (100.0%) | Elapsed: 48s === 开放端口详情 === 地址 服务类型 Banner信息 59.110.163.36:22 ssh | SSH-2.0-OpenSSH_8.0 SSH-2.0-OpenSSH_8.0 59.110.163.36:21 ftp | 220---------- Welcome to Pure-FTPd [privsep] [TLS] ---------- 220---------- Welcome to Pure-FTPd [privsep] [TLS]... 59.110.163.36:888 http(无响应) [无Banner响应] 59.110.163.36:80 http(无响应) [无Banner响应] 59.110.163.36:23333 unknown [无Banner响应] 59.110.163.36:30957 unknown [无Banner响应]
无论超时时长设置多少都是接收不到 banner 响应。
现代码 后来没有办法交给 ai,在不知道多少次的拷打后,产生如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 package mainimport ( "bufio" "fmt" "net" "strings" "time" )var protocolConfig = map [int ]struct { probe string timeout time.Duration identifier func (string ) string }{ 21 : {"USER anonymous\r\n" , 5 * time.Second, identifyFTP}, 22 : {"SSH-2.0-GoScan\r\n" , 3 * time.Second, identifySSH}, 80 : {"" , 8 * time.Second, identifyHTTP}, 443 : {"" , 8 * time.Second, identifyHTTP}, 3306 : {"" , 5 * time.Second, identifyMySQL}, 902 : {"" , 3 * time.Second, identifyVMware}, 912 : {"" , 3 * time.Second, identifyVMware}, }func ReadBanner (conn net.Conn) string { remoteAddr := conn.RemoteAddr().(*net.TCPAddr) port := remoteAddr.Port setupConnection(conn, port) if banner := tryReadBanner(conn); banner != "" { return cleanResponse(banner) } if cfg, ok := protocolConfig[port]; ok { return probeProtocol(conn, port, cfg.probe, cfg.timeout) } return probeDefault(conn) }func setupConnection (conn net.Conn, port int ) { timeout := 3 * time.Second if cfg, ok := protocolConfig[port]; ok { timeout = cfg.timeout } conn.SetReadDeadline(time.Now().Add(timeout)) if tcpConn, ok := conn.(*net.TCPConn); ok { tcpConn.SetKeepAlive(true ) tcpConn.SetKeepAlivePeriod(30 * time.Second) } }func tryReadBanner (conn net.Conn) string { buf := make ([]byte , 1024 ) n, err := conn.Read(buf) if err == nil && n > 0 { return string (buf[:n]) } return "" }func probeProtocol (conn net.Conn, port int , probe string , timeout time.Duration) string { switch port { case 80 , 443 , 8888 : return probeHTTP(conn) default : if probe != "" { conn.Write([]byte (probe)) } return readWithTimeout(conn, timeout) } }func probeHTTP (conn net.Conn) string { req := fmt.Sprintf( "GET / HTTP/1.1\r\n" + "Host: %s\r\n" + "User-Agent: Mozilla/5.0 (compatible; GoScanner/1.0)\r\n" + "Accept: */*\r\n" + "Connection: close\r\n\r\n" , conn.RemoteAddr().(*net.TCPAddr).IP.String(), ) conn.Write([]byte (req)) reader := bufio.NewReader(conn) var resp strings.Builder for { line, err := reader.ReadString('\n' ) if err != nil || line == "\r\n" { break } resp.WriteString(line) } return resp.String() }func buildHTTPRequest (host string ) string { return fmt.Sprintf( "GET / HTTP/1.1\r\n" + "Host: %s\r\n" + "User-Agent: Mozilla/5.0\r\n" + "Connection: close\r\n\r\n" , host, ) }func probeDefault (conn net.Conn) string { conn.Write([]byte ("\x01\x02\x03\x04\n" )) return readWithTimeout(conn, 1 *time.Second) }func readWithTimeout (conn net.Conn, timeout time.Duration) string { conn.SetReadDeadline(time.Now().Add(timeout)) buf := make ([]byte , 1024 ) n, _ := conn.Read(buf) return string (buf[:n]) }func cleanResponse (resp string ) string { resp = strings.Map(func (r rune ) rune { if r >= 32 && r < 127 || r == '\n' || r == '\r' { return r } return -1 }, resp) if len (resp) > 200 { return resp[:200 ] + "..." } return resp }func IdentifyService (banner string , port int ) string { banner = cleanResponse(banner) if cfg, ok := protocolConfig[port]; ok { return cfg.identifier(banner) } switch { case strings.Contains(banner, "HTTP/" ): return identifyHTTP(banner) case port == 80 || port == 443 || port == 8080 || port == 8888 : return "http" default : return "unknown" } }func identifyHTTP (banner string ) string { if server := ExtractHeader(banner, "Server" ); server != "" { switch { case strings.Contains(server, "nginx" ): return "nginx" case strings.Contains(server, "Apache" ): return "apache" case strings.Contains(server, "Microsoft-IIS" ): return "iis" case strings.Contains(server, "lighttpd" ): return "lighttpd" case strings.Contains(server, "Caddy" ): return "caddy" } } if strings.Contains(banner, "nginx" ) { return "nginx" } if strings.Contains(banner, "Apache" ) { return "apache" } return "http-unknown" }func identifyFTP (banner string ) string { if strings.Contains(banner, "Pure-FTPd" ) { return "pure-ftpd" } return "ftp" }func identifySSH (banner string ) string { if strings.Contains(banner, "OpenSSH" ) { return "openssh" } return "ssh" }func identifyMySQL (banner string ) string { return "mysql" }func identifyVMware (banner string ) string { return "vmware-auth" }
这段代码不仅仅可以识别出 http 的 banner 信息,还可以识别出相应的服务,如apache和nginx(当然,识别出 http 具体的服务是后面才加的功能,同时也对 ssh 服务识别做出了对应的优化)
实际上还是没有很懂为什么这段就行而上面不行。
经过思考,可能是因为对不同的服务有不同的代码去负责,有特定的针对性,但可能只是一个方面的原因
不过这样很成功。(有时可能也会出一点小问题)
运行结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Please enter your ip:59.110.163.36 [-] 59.110.163.36:15 is Timeout [-] 59.110.163.36:2800 is Timeout [-] 59.110.163.36:33 is Timeout Scanning: 65535/65535 (100.0%) | Elapsed: 48s === 开放端口详情 === 地址 服务类型 Banner信息 59.110.163.36:21 pure-ftpd 220---------- Welcome to Pure-FTPd [privsep] [TLS]... 59.110.163.36:22 openssh SSH-2.0-OpenSSH_8.0 59.110.163.36:81 nginx HTTP/1.1 400 Bad Request Server: nginx Date: Sun... 59.110.163.36:888 nginx HTTP/1.1 400 Bad Request Server: nginx Date: Sun... 59.110.163.36:80 http-unknown [无Banner响应] 59.110.163.36:23333 nginx HTTP/1.1 400 Bad Request Server: nginx Date: Sun... 59.110.163.36:30957 unknown [无Banner响应]
四、扫描进程展示 这其实是一段无关紧要的部分,只不过我看没有进度展示的运行感觉难受,再等待的过程中受到不知道自己的代码是否可性,需要多长时间,进度到哪里了,写的代码到底对不对等等问题的烦恼,于是就写了一段用于进度显示的函数
1 2 3 4 5 6 7 8 func printProgress (current, total int , start time.Time) { percent := float64 (current) / float64 (total) * 100 elapsed := time.Since(start).Round(time.Second) fmt.Printf("\rScanning: %d/%d (%.1f%%) | Elapsed: %v" , current, total, percent, elapsed) }
运用是在上述端口扫描部分所展示的 run 函数部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for result := range results { scanned++ if scanned%100 == 0 || scanned == totalPorts { printProgress(scanned, totalPorts, startTime) } if result.open { openPorts = append (openPorts, result) } else { if errCount[result.errtype] < 3 { fmt.Printf("[-] %s is %s\n" , result.address, result.errtype) } errCount[result.errtype]++ } }
五、存放数据库 连接云sql,将扫描结果存放在 scan_results 表里。
后来要创建一个 banner 指纹信息库,和该库中的 banner 信息匹配后,才将扫描的结果存放在 scan_results 库中
代码展现 实现存入 sql 数据库的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 package mainimport ( "database/sql" "fmt" "net" "strconv" "strings" "time" )func InitDB () (*sql.DB, error ) { db, err := sql.Open("mysql" , "root:mysqlyd0ngAlicloud@tcp(47.113.206.220:3306)/ASM?parseTime=true" ) if err != nil { return nil , fmt.Errorf("数据库连接失败: %v" , err) } db.SetMaxOpenConns(20 ) db.SetMaxIdleConns(10 ) db.SetConnMaxLifetime(5 * time.Minute) if err := db.Ping(); err != nil { return nil , fmt.Errorf("数据库连接测试失败: %v" , err) } return db, nil }func SaveResult (db *sql.DB, result scanResult) error { if !result.open || result.service == "unknown" { return nil } ip, portStr, err := net.SplitHostPort(result.address) if err != nil { return fmt.Errorf("解析地址失败: %v" , err) } serviceType := MatchFingerprint(db, result.banner) if serviceType == "" { serviceType = strings.ToLower(result.service) if strings.HasPrefix(serviceType, "http" ) { serviceType = "http-unknown" } } if len (serviceType) > 255 { serviceType = serviceType[:255 ] } port, err := strconv.Atoi(portStr) if err != nil { return fmt.Errorf("端口转换失败: %v" , err) } banner := result.banner if len (banner) > 65535 { banner = banner[:65535 ] } _, err = db.Exec(` INSERT INTO scan_results (ip, port, service_id, service_type) VALUES (?, ?, (SELECT id FROM banner WHERE service_name = ? LIMIT 1), ?) ON DUPLICATE KEY UPDATE service_id = VALUES(service_id), service_type = VALUES(service_type)` , ip, port, serviceType, serviceType, ) return err }func MatchFingerprint (db *sql.DB, banner string ) string { var serviceName string db.QueryRow(` SELECT service_name FROM service_fingerprints WHERE ? LIKE CONCAT('%', banner_pattern, '%') ORDER BY LENGTH(banner_pattern) DESC LIMIT 1` , banner).Scan(&serviceName) return serviceName }
SQL 数据库的展现 所建造的 banner 库如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + | id | service_name | banner_pattern | description | + | 1 | ssh | SSH-2.0 % | OpenSSH服务 | | 2 | ssh | SSH-1.99 % | 旧版OpenSSH | | 5 | mysql | mysql_native_password% | MySQL认证 | | 7 | unknown | | 未识别服务 | | 8 | apache | Server: Apache/ % | Apache网页服务器 | | 9 | nginx | Server: nginx[/ s][d.]* | Nginx网页服务器 | | 10 | iis | Server: Microsoft- IIS/ % | IIS网页服务器 | | 11 | openssh | SSH-2.0 - OpenSSH% | OpenSSH服务 | | 12 | dropbear | SSH-2.0 - dropbear% | Dropbear SSH服务 | | 13 | pure- ftpd | 220 | 14 | vmware- ftpd | 220 VMware Authentication Daemon% | VMware认证守护进程 | | 15 | mysql | \x0a[0 -9 ]\.\d+ \.\d+ - MySQL | MySQL数据库 | | 16 | redis | \+ REDIS\d+ | Redis数据库 | | 17 | vmware- auth | 220 VMware Authentication Daemon% | VMware认证服务 | | 18 | openssh | SSH-2.0 - OpenSSH[_.d]+ % | OpenSSH服务 | | 19 | pure- ftpd | 220 [- ]+ Welcome to Pure- FTPd% | Pure- FTPd服务 | | 20 | http- service | ^ HTTP/ d.d | 基础HTTP服务 | +
这是一次的扫描结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Please enter your ip:59.110.163.36 [-] 59.110.163.36:2800 is Timeout [-] 59.110.163.36:56 is Timeout [-] 59.110.163.36:68 is Timeout Scanning: 65535/65535 (100.0%) | Elapsed: 48s === 开放端口详情 === 地址 服务类型 Banner信息 59.110.163.36:21 pure-ftpd 220---------- Welcome to Pure-FTPd [privsep] [TLS]... 59.110.163.36:22 openssh SSH-2.0-OpenSSH_8.0 59.110.163.36:81 nginx HTTP/1.1 400 Bad Request Server: nginx Date: Sun... 59.110.163.36:888 nginx HTTP/1.1 400 Bad Request Server: nginx Date: Sun... 59.110.163.36:80 http-unknown [无Banner响应] 59.110.163.36:23333 nginx HTTP/1.1 400 Bad Request Server: nginx Date: Sun... 59.110.163.36:30957 unknown [无Banner响应] 2025/04/20 22:32:27 成功储存 59.110.163.36:21 2025/04/20 22:32:27 成功储存 59.110.163.36:22 2025/04/20 22:32:27 成功储存 59.110.163.36:81 2025/04/20 22:32:28 成功储存 59.110.163.36:888 2025/04/20 22:32:28 成功储存 59.110.163.36:80 2025/04/20 22:32:28 成功储存 59.110.163.36:23333 2025/04/20 22:32:28 成功储存 59.110.163.36:30957
这是扫描结束后存放 scan_results 库的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + | id | ip | port | service_id | service_type | scan_time | + | 1 | 127.0 .0 .1 | 3306 | 5 | mysql | 2025 -04 -19 15 :19 :54 | | 2 | 127.0 .0 .1 | 902 | NULL | vmware- auth | 2025 -04 -19 15 :19 :55 | | 3 | 127.0 .0 .1 | 912 | NULL | vmware- auth | 2025 -04 -19 15 :19 :55 | | 4 | 127.0 .0 .1 | 8888 | 8 | apache | 2025 -04 -19 15 :19 :55 | | 5 | 127.0 .0 .1 | 9210 | NULL | http- unknown | 2025 -04 -19 15 :19 :55 | | 6 | 127.0 .0 .1 | 49743 | NULL | http- unknown | 2025 -04 -19 15 :19 :55 | | 7 | 127.0 .0 .1 | 80 | NULL | http- unknown | 2025 -04 -19 15 :19 :56 | | 15 | 59.110 .163 .36 | 81 | 9 | nginx | 2025 -04 -20 04 :38 :04 | | 16 | 59.110 .163 .36 | 888 | 9 | nginx | 2025 -04 -20 04 :38 :04 | | 17 | 59.110 .163 .36 | 80 | NULL | http- unknown | 2025 -04 -20 04 :38 :04 | | 18 | 59.110 .163 .36 | 23333 | 9 | nginx | 2025 -04 -20 04 :38 :04 | | 19 | 59.110 .163 .36 | 22 | 11 | openssh | 2025 -04 -20 04 :53 :04 | | 20 | 59.110 .163 .36 | 21 | 13 | pure- ftpd | 2025 -04 -20 04 :53 :04 | +
其中 service_id 是识别 banner 库中的结果,如果 banner 信息存于 banner 库中,那么 service_id
则会显示所对应的行数,如果为 NULL,那就是没有接收到 banner 信息或者是 banner 不在 banner 表中,而上表所展示的59.110.163.36的80端口服务信息为 http-unknown ,这就说明了没有接受到 banner 信息,之所以是认定为 http 服务是由端口判断,这些代码逻辑都在上述的指纹识别的代码中。
本周总结 上述基本上都为本周我所完成的结果和学习到的内容,本周学习到的知识很多,之前一直都是去看 go 语言的语法而不是真正的去写代码,实际证明,真正去用 go 语言来写这样的一个项目后,我对 go 语言的的认知才变的更加深刻,上手写代码才更熟练。
同时本周我也学到的更多的网络知识,帮助我完成这样的扫描器。
不过真的很多内容都要有 ai 辅助学习啊,不然根本不会不理解,去一一生啃那些枯燥难懂的内容效率真的很慢。不过这样也导致我在一些方面还没有搞的透彻,还需要加强学习。
接下来要迎接的是下周的内容。
2025.4.27 周报 在这一周中我完成的内容为:
子域名收集(目前只完成了crt.sh和搜索引擎方式收集)
子域名收集功能和端口扫描功能分离选择
子域名与其对应的 IP 和网站标题入库
一、子域名收集 在这个模块中,ai 出力出的更多(悲),先丢个代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 package mainimport ( "context" "encoding/json" "fmt" "io/ioutil" "log" "net" "net/http" "strings" "time" )type CertRecord struct { ID uint64 `json:"id"` LoggedAt string `json:"entry_timestamp"` NotBefore string `json:"not_before"` NotAfter string `json:"not_after"` CommonName string `json:"common_name"` NameValue string `json:"name_value"` MatchingIPs []net.IP IsWildcard bool }type CollectResult struct { Subdomain string IPs []net.IP FirstSeen time.Time Sources []string }type CRTshCollector struct {}func (c *CRTshCollector) Collect(domain string , timeout time.Duration) ([]CollectResult, error ) { return CollectSubdomains_crtsh(domain, timeout) }func CollectSubdomains_crtsh (domain string , timeout time.Duration) ([]CollectResult, error ) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() apiURL := fmt.Sprintf("https://crt.sh/?q=%s&output=json" , domain) log.Printf("正在查询: %s" , apiURL) records, err := fetchCertRecords(ctx, apiURL) if err != nil { return nil , fmt.Errorf("fetchCertRecords失败: %w" , err) } if len (records) == 0 { return nil , fmt.Errorf("未找到任何证书记录" ) } var results []CollectResult for _, rec := range records { select { case <-ctx.Done(): return results, nil default : res := processRecord(rec, domain) if res.Subdomain != "" { results = append (results, res) } } } return results, nil }func fetchCertRecords (ctx context.Context, apiURL string ) ([]CertRecord, error ) { req, err := http.NewRequestWithContext(ctx, "GET" , apiURL, nil ) if err != nil { return nil , fmt.Errorf("创建请求失败: %w" , err) } req.Header.Set("User-Agent" , "Mozilla/5.0 (compatible; MyScanner/1.0)" ) req.Header.Set("Accept" , "application/json" ) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return nil , fmt.Errorf("请求失败: %w" , err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := ioutil.ReadAll(resp.Body) return nil , fmt.Errorf("API返回错误: %s\n响应: %s" , resp.Status, string (body)) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil , fmt.Errorf("读取响应失败: %w" , err) } log.Printf("API响应: %s" , string (body[:min(200 , len (body))])) var records []CertRecord if err := json.Unmarshal(body, &records); err != nil { return nil , fmt.Errorf("JSON解析失败: %w\n响应: %s" , err, string (body[:200 ])) } return records, nil }func processRecord (rec CertRecord, baseDomain string ) CollectResult { rec.IsWildcard = strings.HasPrefix(rec.CommonName, "*." ) || strings.Contains(rec.NameValue, "*." ) names := append (strings.Split(rec.NameValue, "\n" ), rec.CommonName) var validSubdomains []string for _, name := range names { name = strings.ToLower(strings.TrimSpace(name)) if name != "" && isValidSubdomain(name, baseDomain) { validSubdomains = append (validSubdomains, name) } } if len (validSubdomains) == 0 { return CollectResult{} } subdomain := validSubdomains[0 ] var ips []net.IP if !rec.IsWildcard { if resolvedIPs, err := net.LookupIP(subdomain); err == nil { ips = resolvedIPs } } var firstSeen time.Time if rec.LoggedAt != "" { firstSeen, _ = time.Parse("2006-01-02T15:04:05" , rec.LoggedAt) } return CollectResult{ Subdomain: subdomain, IPs: ips, FirstSeen: firstSeen, Sources: []string {"crt.sh" }, } }func isValidSubdomain (name, baseDomain string ) bool { name = strings.TrimSuffix(name, "." ) baseDomain = strings.TrimSuffix(baseDomain, "." ) return name == baseDomain || strings.HasSuffix(name, "." +baseDomain) }
这是在证书透明收集的代码实现,经检验有效果
下面是在main.go中整合的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 func collectSubdomains (db *sql.DB, scan scanner) { var domain string fmt.Print("please enter your domain: " ) fmt.Scan(&domain) var results []CollectResult if crtResults, err := (&CRTshCollector{}).Collect(domain, 30 *time.Second); err == nil { results = append (results, crtResults...) } if searchResults, err := (&SearchEngineCollector{}).Collect(domain, 30 *time.Second); err == nil { fmt.Printf("[DEBUG] 搜索引擎结果数量: %d\n" , len (searchResults)) results = append (results, searchResults...) } else { log.Printf("搜索引擎收集错误: %v" , err) } uniqueIPs := make (map [string ]bool ) var ipsToScan []string for _, res := range results { fmt.Printf("[%s] %s (IPs: %v)\n" , res.FirstSeen.Format("2006-01-02" ), res.Subdomain, res.IPs) domainID, err := SaveDomainInfo(db, domain, res.Subdomain, strings.HasPrefix(res.Subdomain, "*." ), "" , strings.Join(res.Sources, "," ), ) if err != nil { log.Printf("保存子域名失败 %s: %v" , res.Subdomain, err) continue } for _, ip := range res.IPs { ipStr := ip.String() if !uniqueIPs[ipStr] && ip.To4() != nil { uniqueIPs[ipStr] = true ipsToScan = append (ipsToScan, ipStr) if err := SaveDomainIP(db, domainID, ipStr, res.Subdomain, nil ); err != nil { log.Printf("保存IP关联失败 %s: %v" , ipStr, err) } } } } var answer string fmt.Print("Subdomain collecting is done. Do the domains need to scan?(y/N): " ) fmt.Scan(&answer) if answer == "y" { for _, scan.ip = range ipsToScan { domainScan(scan, db) } } else if answer == "n" { fmt.Print("The task is over." ) } }
为了将域名扫描的结果和端口扫描的结果分开,我又将这些整合到不同的函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func domainScan (scan scanner, db *sql.DB) { fmt.Printf("\n=== 开始域名扫描 %s ===\n" , scan.ip) if IsHostAlive(scan.ip) { openPorts := Run(scan.ip, scan.network) if err := SaveDomainScanResult(db, scan.ip, openPorts); err != nil { log.Printf("保存扫描结果失败: %v" , err) } } else { fmt.Println("Can't ping" ) if IsHostAlive_TCP(scan.ip) { openPorts := Run(scan.ip, scan.network) if err := SaveDomainScanResult(db, scan.ip, openPorts); err != nil { log.Printf("保存扫描结果失败: %v" , err) } } else { log.Print("没有进入TCP连接" ) fmt.Printf("%s is not alive\n" , scan.ip) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func portScan (scan scanner, db *sql.DB) { if IsHostAlive(scan.ip) { openPorts := Run(scan.ip, scan.network) for _, result := range openPorts { if err := SaveResult(db, result); err != nil { log.Printf("存储失败 %s: %v" , result.address, err) } else { log.Printf("成功储存 %s" , result.address) } } } else { fmt.Println("Can't ping" ) if IsHostAlive_TCP(scan.ip) { openPorts := Run(scan.ip, scan.network) for _, result := range openPorts { if err := SaveResult(db, result); err != nil { log.Printf("存储失败 %s: %v" , result.address, err) } else { log.Printf("成功储存 %s" , result.address) } } } else { log.Print("没有进入TCP连接" ) fmt.Printf("%s is not alive\n" , scan.ip) } } }
而这里就是我的main函数实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func main () { db, err := InitDB() if err != nil { log.Fatal(err) } defer db.Close() var scan scanner scan.network = "tcp" var command string fmt.Print("please enter a command(domain/scan): " ) fmt.Scan(&command) if command == "scan" { fmt.Print("Please enter your ip:" ) fmt.Scan(&scan.ip) portScan(scan, db) } else if command == "domain" { collectSubdomains(db, scan) } else { fmt.Println("please enter a true command." ) } }
二、对应结果入库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 package mainimport ( "database/sql" "encoding/json" "fmt" "log" "net" "regexp" "strconv" "strings" "time" )func InitDB () (*sql.DB, error ) { db, err := sql.Open("mysql" , "root:mysqlyd0ngAlicloud@tcp(47.113.206.220:3306)/ASM?parseTime=true" ) if err != nil { return nil , fmt.Errorf("数据库连接失败: %v" , err) } db.SetMaxOpenConns(20 ) db.SetMaxIdleConns(10 ) db.SetConnMaxLifetime(5 * time.Minute) if err := db.Ping(); err != nil { return nil , fmt.Errorf("数据库连接测试失败: %v" , err) } return db, nil }func SaveResult (db *sql.DB, result scanResult) error { if !result.open || result.service == "unknown" { return nil } ip, portStr, err := net.SplitHostPort(result.address) if err != nil { return fmt.Errorf("解析地址失败: %v" , err) } port, err := strconv.Atoi(portStr) if err != nil { return fmt.Errorf("端口转换失败: %v" , err) } serviceType := MatchFingerprint(db, result.banner, port) if serviceType == "" { serviceType = strings.ToLower(result.service) if strings.HasPrefix(serviceType, "http" ) { serviceType = "http-unknown" } } if len (serviceType) > 255 { serviceType = serviceType[:255 ] } banner := result.banner if len (banner) > 65535 { banner = banner[:65535 ] } _, err = db.Exec(` INSERT INTO scan_results (ip, port, service_id, service_type) VALUES (?, ?, (SELECT id FROM banner WHERE service_name = ? LIMIT 1), ?) ON DUPLICATE KEY UPDATE service_id = VALUES(service_id), service_type = VALUES(service_type)` , ip, port, serviceType, serviceType, ) return err }func MatchFingerprint (db *sql.DB, banner string , port int ) string { var serviceName string err := db.QueryRow(` SELECT description FROM finger_print WHERE match_type = 'regex' AND ? REGEXP keyword AND protocol_type = 'TCP' ORDER BY CASE WHEN description IN ('ssh', 'ftp', 'mysql', 'http', 'nginx', 'apache', 'iis') THEN 0 ELSE 1 END, LENGTH(keyword) DESC LIMIT 1` , banner).Scan(&serviceName) if err == nil && serviceName != "" { return serviceName } if strings.Contains(banner, "HTTP/" ) { err := db.QueryRow(` SELECT service_name FROM finger_print WHERE match_type = 'regex' AND ? REGEXP keyword AND service_name IN ('nginx','apache','iis','http') ORDER BY LENGTH(keyword) DESC LIMIT 1` , banner).Scan(&serviceName) if err == nil && serviceName != "" { log.Printf("DEBUG - HTTP服务匹配成功: %s" , serviceName) return serviceName } } err = db.QueryRow(` SELECT service_name FROM finger_print WHERE match_type = 'regex' AND ? REGEXP keyword AND (protocol_type = 'TCP' OR protocol_type IS NULL) ORDER BY CASE WHEN service_name IN ('ssh', 'ftp','mysql') THEN 0 ELSE 1 END, -- 优先匹配常见服务 LENGTH(keyword) DESC -- 其次匹配更长/更精确的模式 LIMIT 1` , banner).Scan(&serviceName) if err == nil && serviceName != "" { return serviceName } err = db.QueryRow(` SELECT service_name FROM finger_print WHERE match_type = 'port' AND keyword = ? LIMIT 1` , strconv.Itoa(port)).Scan(&serviceName) if err == nil && serviceName != "" { return serviceName } err = db.QueryRow(` SELECT service_name FROM finger_print WHERE match_type = 'regex' AND ? LIKE CONCAT('%', keyword, '%') AND (protocol_type = 'TCP' OR protocol_type IS NULL) ORDER BY LENGTH(keyword) DESC LIMIT 1` , banner).Scan(&serviceName) if err == nil && serviceName != "" { return serviceName } return "" }
在这段代码中和 finger_print 库匹配的代码中还是有一些问题的,关乎指纹识别方面,等着来解决。
本周总结 这一周完成的成果不如上一周的成果多,可能也与这周的事情相对较多,这篇文章也写的迟,写的潦草,总之拖拖拖拖到现在