fscan/core/WebDetection.go
ZacharyZcR d19abcac36 feat: 新增发包频率控制功能
- 添加-rate参数:控制每分钟最大发包次数
- 添加-maxpkts参数:控制整个程序最大发包总数
- 在所有网络操作点集成发包限制检查
- 支持端口扫描、Web检测、服务插件、POC扫描等场景
- 默认不限制,保持向后兼容性
2025-09-02 11:24:09 +00:00

545 lines
16 KiB
Go
Raw Permalink 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 {
// 0. 最优先路径检查是否已通过服务指纹识别为Web服务
if IsMarkedWebService(host, port) {
serviceInfo, _ := GetWebServiceInfo(host, port)
common.LogDebug(fmt.Sprintf("端口 %d 通过服务指纹已识别为Web服务: %s", port, serviceInfo.Name))
return true
}
// 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服务回退到HTTP协议探测", port))
result := w.detectHTTPService(host, port)
// 4. 缓存结果
w.cacheMutex.Lock()
w.detectionCache[cacheKey] = result
w.cacheMutex.Unlock()
common.LogDebug(fmt.Sprintf("端口 %d HTTP协议探测结果: %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", "*/*")
// 检查发包限制
if canSend, reason := common.CanSendPacket(); !canSend {
common.LogError(fmt.Sprintf("HTTP请求 %s 受限: %s", req.URL.String(), reason))
return false
}
resp, err := client.Do(req)
if err != nil {
// HTTP请求失败计为TCP失败
common.IncrementTCPFailedPacketCount()
// 检查错误类型某些错误也表明是HTTP服务
return w.analyzeHTTPError(err)
}
defer resp.Body.Close()
// HTTP请求成功计为TCP成功
common.IncrementTCPSuccessPacketCount()
// 分析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
}
// ===============================
// 统一服务识别架构 - 基于指纹的Web服务判断
// ===============================
// WebServiceCache Web服务缓存用于存储已识别的Web服务信息
var (
webServiceCache = make(map[string]*ServiceInfo)
webCacheMutex sync.RWMutex
)
// IsWebServiceByFingerprint 基于服务指纹智能判断是否为Web服务
func IsWebServiceByFingerprint(serviceInfo *ServiceInfo) bool {
if serviceInfo == nil || serviceInfo.Name == "" {
return false
}
serviceName := strings.ToLower(serviceInfo.Name)
// HTTP相关服务类型列表基于nmap服务指纹库
webServices := []string{
"http", "https", "http-proxy", "http-alt", "ssl/http",
"http-mgmt", "httpd", "nginx", "apache", "iis",
"tomcat", "jetty", "lighttpd", "caddy", "traefik",
"websphere", "weblogic", "jboss", "wildfly",
"nodejs", "express", "kestrel", "gunicorn", "uvicorn",
"cherrypy", "tornado", "flask", "django", "rails",
"php", "php-fpm", "asp", "aspx", "jsp", "servlet",
"cgid", "cgi", "fcgi", "wsgi", "node.js",
}
// 检查服务名称是否匹配Web服务
for _, webService := range webServices {
if serviceName == webService || strings.Contains(serviceName, webService) {
common.LogDebug(fmt.Sprintf("基于指纹识别为Web服务: %s -> %s", serviceName, webService))
return true
}
}
// 检查Banner中的Web服务器特征
if serviceInfo.Banner != "" {
banner := strings.ToLower(serviceInfo.Banner)
webBanners := []string{
"server:", "apache", "nginx", "iis", "lighttpd",
"caddy", "traefik", "tomcat", "jetty", "nodejs",
"content-type:", "content-length:", "http/",
"connection:", "keep-alive", "close",
}
for _, webBanner := range webBanners {
if strings.Contains(banner, webBanner) {
common.LogDebug(fmt.Sprintf("基于Banner识别为Web服务: %s", webBanner))
return true
}
}
}
// 检查额外信息中的Web特征
if serviceInfo.Extras != nil {
for key, value := range serviceInfo.Extras {
keyLower := strings.ToLower(key)
valueLower := strings.ToLower(value)
if keyLower == "info" && (strings.Contains(valueLower, "web") ||
strings.Contains(valueLower, "http") || strings.Contains(valueLower, "html")) {
common.LogDebug(fmt.Sprintf("基于Extras识别为Web服务: %s=%s", key, value))
return true
}
}
}
common.LogDebug(fmt.Sprintf("指纹识别为非Web服务: %s", serviceName))
return false
}
// MarkAsWebService 标记端口为Web服务用于后续Web插件调用
func MarkAsWebService(host string, port int, serviceInfo *ServiceInfo) {
cacheKey := fmt.Sprintf("%s:%d", host, port)
webCacheMutex.Lock()
defer webCacheMutex.Unlock()
webServiceCache[cacheKey] = serviceInfo
common.LogDebug(fmt.Sprintf("标记Web服务: %s [%s]", cacheKey, serviceInfo.Name))
}
// GetWebServiceInfo 获取已标记的Web服务信息
func GetWebServiceInfo(host string, port int) (*ServiceInfo, bool) {
cacheKey := fmt.Sprintf("%s:%d", host, port)
webCacheMutex.RLock()
defer webCacheMutex.RUnlock()
serviceInfo, exists := webServiceCache[cacheKey]
return serviceInfo, exists
}
// IsMarkedWebService 检查端口是否已被标记为Web服务
func IsMarkedWebService(host string, port int) bool {
_, exists := GetWebServiceInfo(host, port)
return exists
}