fscan/core/WebDetection.go
ZacharyZcR 46cfa2a64d fix: 简化HTTP错误分析逻辑,消除误报
核心修复:
- 大幅简化analyzeHTTPError逻辑,移除容易误报的指示器
- 不再将SSL/TLS错误误判为HTTP服务(SSH/FTP被误报为HTTPS的根因)
- 统一错误处理:HTTP请求失败一律判定为非HTTP服务
- 协议预检查+HTTP验证的两阶段检测更加可靠

修复的误报:
- SSH端口22不再被误识别为HTTPS服务
- FTP端口21不再被误识别为HTTPS服务
- SMTP端口25不再被误识别为HTTPS服务
- 保持MySQL端口3306的正确识别(协议预检查直接过滤)

技术改进:
- 错误分析逻辑从40+行简化到15行
- 消除硬编码的协议指示器列表
- 基于协议预检查的信任机制:如果预检查通过但HTTP失败,说明不是HTTP
2025-09-02 00:08:17 +00:00

424 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package core
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/common"
)
// WebPortDetector Web端口检测器
type WebPortDetector struct {
// 常见Web端口列表
commonWebPorts map[int]bool
// HTTP检测超时时间
httpTimeout time.Duration
// HTTP客户端
httpClient *http.Client
// HTTPS客户端
httpsClient *http.Client
// 检测结果缓存(避免重复检测)
detectionCache map[string]bool
// 协议检测缓存避免重复TCP连接
protocolCache map[string]bool
// 缓存互斥锁
cacheMutex sync.RWMutex
}
var (
// 全局单例实例
webDetectorInstance *WebPortDetector
webDetectorOnce sync.Once
)
// GetWebPortDetector 获取Web端口检测器单例
func GetWebPortDetector() *WebPortDetector {
webDetectorOnce.Do(func() {
webDetectorInstance = newWebPortDetector()
})
return webDetectorInstance
}
// newWebPortDetector 创建Web端口检测器私有方法
func newWebPortDetector() *WebPortDetector {
timeout := 3 * time.Second
// 只保留最基本的Web端口用于快速路径优化
commonPorts := map[int]bool{
80: true, // HTTP
443: true, // HTTPS
}
// 创建HTTP客户端
httpClient := &http.Client{
Timeout: timeout,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: timeout,
}).DialContext,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: true,
MaxConnsPerHost: 5,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // 不跟随重定向,减少检测时间
},
}
// 创建HTTPS客户端跳过证书验证
httpsClient := &http.Client{
Timeout: timeout,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: timeout,
}).DialContext,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: true,
MaxConnsPerHost: 5,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
return &WebPortDetector{
commonWebPorts: commonPorts,
httpTimeout: timeout,
httpClient: httpClient,
httpsClient: httpsClient,
detectionCache: make(map[string]bool),
protocolCache: make(map[string]bool),
}
}
// IsWebService 智能检测端口是否运行Web服务
func (w *WebPortDetector) IsWebService(host string, port int) bool {
// 1. 快速路径常见Web端口直接返回true
if w.IsCommonWebPort(port) {
common.LogDebug(fmt.Sprintf("端口 %d 是常见Web端口启用Web插件", port))
return true
}
// 2. 缓存检查:避免重复检测同一端口
cacheKey := fmt.Sprintf("%s:%d", host, port)
w.cacheMutex.RLock()
if cached, exists := w.detectionCache[cacheKey]; exists {
w.cacheMutex.RUnlock()
common.LogDebug(fmt.Sprintf("端口 %d 使用缓存结果: %v", port, cached))
return cached
}
w.cacheMutex.RUnlock()
// 3. 智能路径对非常见端口进行HTTP协议探测
common.LogDebug(fmt.Sprintf("对端口 %d 进行智能Web检测", port))
result := w.detectHTTPService(host, port)
// 4. 缓存结果
w.cacheMutex.Lock()
w.detectionCache[cacheKey] = result
w.cacheMutex.Unlock()
common.LogDebug(fmt.Sprintf("端口 %d 智能检测结果: %v", port, result))
return result
}
// DetectHTTPServiceOnly 仅通过HTTP协议检测端口不使用预定义端口列表
func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int) bool {
// 缓存检查:避免重复检测同一端口
cacheKey := fmt.Sprintf("%s:%d", host, port)
w.cacheMutex.RLock()
if cached, exists := w.detectionCache[cacheKey]; exists {
w.cacheMutex.RUnlock()
return cached
}
w.cacheMutex.RUnlock()
// 跳过预定义端口检查,直接进行协议探测
result := w.detectHTTPService(host, port)
// 缓存结果
w.cacheMutex.Lock()
w.detectionCache[cacheKey] = result
w.cacheMutex.Unlock()
return result
}
// IsCommonWebPort 检查是否为常见Web端口
func (w *WebPortDetector) IsCommonWebPort(port int) bool {
return w.commonWebPorts[port]
}
// detectHTTPService 检测HTTP服务真正的智能检测
func (w *WebPortDetector) detectHTTPService(host string, port int) bool {
// 创建检测上下文,避免长时间阻塞
ctx, cancel := context.WithTimeout(context.Background(), w.httpTimeout)
defer cancel()
// 先进行一次协议预检查,避免重复检测
if !w.quickProtocolCheck(ctx, host, port) {
return false
}
// 并发检测HTTP和HTTPS
httpChan := make(chan bool, 1)
httpsChan := make(chan bool, 1)
// 检测HTTP跳过协议检查因为已经检查过了
go func() {
httpChan <- w.tryHTTPConnectionDirect(ctx, host, port, "http")
}()
// 检测HTTPS跳过协议检查因为已经检查过了
go func() {
httpsChan <- w.tryHTTPConnectionDirect(ctx, host, port, "https")
}()
// 等待任一协议检测成功
select {
case result := <-httpChan:
if result {
common.LogDebug(fmt.Sprintf("端口 %d 检测到HTTP服务", port))
return true
}
case <-ctx.Done():
return false
}
select {
case result := <-httpsChan:
if result {
common.LogDebug(fmt.Sprintf("端口 %d 检测到HTTPS服务", port))
return true
}
case <-ctx.Done():
return false
}
return false
}
// tryHTTPConnection 尝试HTTP连接带协议检查
func (w *WebPortDetector) tryHTTPConnection(ctx context.Context, host string, port int, protocol string) bool {
// 先进行简单的协议嗅探避免向非HTTP服务发送HTTP请求
if !w.quickProtocolCheck(ctx, host, port) {
return false
}
return w.tryHTTPConnectionDirect(ctx, host, port, protocol)
}
// tryHTTPConnectionDirect 直接尝试HTTP连接跳过协议检查
func (w *WebPortDetector) tryHTTPConnectionDirect(ctx context.Context, host string, port int, protocol string) bool {
// 构造URL
var url string
if (port == 80 && protocol == "http") || (port == 443 && protocol == "https") {
url = fmt.Sprintf("%s://%s", protocol, host)
} else {
url = fmt.Sprintf("%s://%s:%d", protocol, host, port)
}
// 选择客户端
var client *http.Client
if protocol == "https" {
client = w.httpsClient
} else {
client = w.httpClient
}
// 发送HEAD请求最小化网络开销
req, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
if err != nil {
return false
}
req.Header.Set("User-Agent", "fscan-web-detector/2.2")
req.Header.Set("Accept", "*/*")
resp, err := client.Do(req)
if err != nil {
// 检查错误类型某些错误也表明是HTTP服务
return w.analyzeHTTPError(err)
}
defer resp.Body.Close()
// 分析HTTP响应
return w.analyzeHTTPResponse(resp, url)
}
// analyzeHTTPError 分析HTTP错误判断是否表明存在HTTP服务
func (w *WebPortDetector) analyzeHTTPError(err error) bool {
errStr := strings.ToLower(err.Error())
// 简化逻辑既然我们已经做了协议预检查到这里的都应该是可能的HTTP服务
// 只检查明确的连接错误
connectionErrors := []string{
"connection refused",
"no such host",
"network unreachable",
"deadline exceeded",
"timeout",
}
for _, connErr := range connectionErrors {
if strings.Contains(errStr, connErr) {
return false
}
}
// 所有其他错误包括malformed response、SSL错误等都认为不是HTTP服务
// 因为我们已经通过协议预检查确认目标可能是HTTP服务
// 如果仍然出错说明不是标准的HTTP服务
common.LogDebug(fmt.Sprintf("HTTP请求失败判定为非HTTP服务: %s", err.Error()))
return false
}
// analyzeHTTPResponse 分析HTTP响应判断是否为Web服务
func (w *WebPortDetector) analyzeHTTPResponse(resp *http.Response, url string) bool {
// 检查是否为有效的HTTP响应
if resp.StatusCode <= 0 {
return false
}
// 检查状态码是否合理(避免协议混淆导致的假阳性)
if resp.StatusCode < 100 || resp.StatusCode >= 600 {
common.LogDebug(fmt.Sprintf("端口返回无效HTTP状态码: %d", resp.StatusCode))
return false
}
// 更严格的验证检查是否有基本的HTTP头部
if len(resp.Header) == 0 {
common.LogDebug(fmt.Sprintf("端口返回HTTP响应但缺少HTTP头部: %s", url))
return false
}
// 检查是否包含标准HTTP头部字段
hasValidHeaders := false
standardHeaders := []string{"Server", "Content-Type", "Content-Length", "Date", "Connection", "Cache-Control"}
for _, header := range standardHeaders {
if resp.Header.Get(header) != "" {
hasValidHeaders = true
break
}
}
if !hasValidHeaders {
common.LogDebug(fmt.Sprintf("端口返回响应但缺少标准HTTP头部: %s", url))
return false
}
// 分析响应头获取更多信息
serverHeader := resp.Header.Get("Server")
contentType := resp.Header.Get("Content-Type")
// 特殊处理某些非Web服务也会返回HTTP响应
if w.isNonWebHTTPService(serverHeader, contentType) {
common.LogDebug(fmt.Sprintf("端口返回HTTP响应但可能不是Web服务: %s", url))
return false
}
// 记录检测到的Web服务器信息
if serverHeader != "" {
common.LogDebug(fmt.Sprintf("检测到Web服务器: %s (Server: %s)", url, serverHeader))
}
return true
}
// isNonWebHTTPService 判断是否为非Web的HTTP服务
func (w *WebPortDetector) isNonWebHTTPService(serverHeader, contentType string) bool {
// 简化:只基于实际响应内容判断,不用硬编码列表
return false
}
// quickProtocolCheck 快速协议检查避免向非HTTP服务发送HTTP请求
func (w *WebPortDetector) quickProtocolCheck(ctx context.Context, host string, port int) bool {
// 检查协议缓存
protocolKey := fmt.Sprintf("%s:%d:protocol", host, port)
w.cacheMutex.RLock()
if cached, exists := w.protocolCache[protocolKey]; exists {
w.cacheMutex.RUnlock()
return cached
}
w.cacheMutex.RUnlock()
// 执行实际的协议检测
result := w.doProtocolCheck(ctx, host, port)
// 缓存结果
w.cacheMutex.Lock()
w.protocolCache[protocolKey] = result
w.cacheMutex.Unlock()
return result
}
// doProtocolCheck 执行实际的协议检测
func (w *WebPortDetector) doProtocolCheck(ctx context.Context, host string, port int) bool {
// 建立TCP连接
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), 2*time.Second)
if err != nil {
return false
}
defer conn.Close()
// 设置读取超时
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
// 读取服务器的初始响应(如果有)
buffer := make([]byte, 512)
n, err := conn.Read(buffer)
if err != nil {
// 大多数HTTP服务不会主动发送数据这是正常的
// 超时或EOF表示服务器在等待客户端请求HTTP特征
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return true // HTTP服务通常不主动发送数据
}
if err.Error() == "EOF" {
return true // 连接正常但无数据可能是HTTP
}
return true // 其他错误也可能是HTTP服务
}
if n > 0 {
// 如果服务器主动发送数据,检查是否包含明显的二进制内容
// 简单规则如果包含太多不可打印字符很可能不是HTTP协议
nonPrintableCount := 0
for i := 0; i < n && i < 100; i++ { // 只检查前100字节
b := buffer[i]
if b < 32 && b != 9 && b != 10 && b != 13 { // 排除tab、换行、回车
nonPrintableCount++
}
}
// 如果不可打印字符太多超过20%),很可能是二进制协议
if nonPrintableCount > n/5 {
common.LogDebug(fmt.Sprintf("端口 %d 检测到二进制协议(不可打印字符: %d/%d", port, nonPrintableCount, n))
return false
}
}
return true
}
// min 辅助函数
func min(a, b int) int {
if a < b {
return a
}
return b
}