mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00

- 添加-rate参数:控制每分钟最大发包次数 - 添加-maxpkts参数:控制整个程序最大发包总数 - 在所有网络操作点集成发包限制检查 - 支持端口扫描、Web检测、服务插件、POC扫描等场景 - 默认不限制,保持向后兼容性
545 lines
16 KiB
Go
545 lines
16 KiB
Go
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
|
||
}
|
||
|
||
|