feat: 实现智能Web服务检测系统

- 扩展Web端口识别范围至40+个常见端口
- 新增HTTP/HTTPS协议智能探测机制
- 实现并发协议检测提升性能
- 添加精确错误分析避免误判
- 完善插件过滤确保Web插件仅在检测到Web服务时执行
- 支持端口模式匹配和响应头分析
This commit is contained in:
ZacharyZcR 2025-08-12 14:20:57 +08:00
parent 0cc29afbeb
commit 338dd60c3e
3 changed files with 347 additions and 28 deletions

View File

@ -108,7 +108,8 @@ func (b *BaseScanStrategy) isPluginTypeMatchedByName(pluginName string) bool {
case FilterLocal: case FilterLocal:
return metadata.Category == "local" return metadata.Category == "local"
case FilterService: case FilterService:
return metadata.Category == "service" // 服务扫描允许service类型以及通过智能检测的web类型在上层逻辑中处理
return metadata.Category == "service" || metadata.Category == "web"
case FilterWeb: case FilterWeb:
return metadata.Category == "web" return metadata.Category == "web"
default: default:
@ -122,6 +123,7 @@ func (b *BaseScanStrategy) isPluginTypeMatched(plugin common.ScanPlugin) bool {
case FilterLocal: case FilterLocal:
return plugin.HasType(common.PluginTypeLocal) return plugin.HasType(common.PluginTypeLocal)
case FilterService: case FilterService:
// 服务扫描排除本地插件允许服务和Web插件
return !plugin.HasType(common.PluginTypeLocal) return !plugin.HasType(common.PluginTypeLocal)
case FilterWeb: case FilterWeb:
return plugin.HasType(common.PluginTypeWeb) return plugin.HasType(common.PluginTypeWeb)
@ -131,7 +133,7 @@ func (b *BaseScanStrategy) isPluginTypeMatched(plugin common.ScanPlugin) bool {
} }
// IsPluginApplicableByName 根据插件名称判断是否适用(新方法) // IsPluginApplicableByName 根据插件名称判断是否适用(新方法)
func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetPort int, isCustomMode bool) bool { func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool) bool {
// 自定义模式下运行所有明确指定的插件 // 自定义模式下运行所有明确指定的插件
if isCustomMode { if isCustomMode {
return true return true
@ -143,7 +145,7 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetPor
} }
// 智能Web插件检测如果是Web插件且检测到Web服务则包含Web插件 // 智能Web插件检测如果是Web插件且检测到Web服务则包含Web插件
if b.shouldIncludeWebPlugin(metadata, targetPort) { if b.shouldIncludeWebPlugin(metadata, targetHost, targetPort) {
return true return true
} }
@ -162,11 +164,16 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetPor
return false return false
} }
// 对于Web插件即使没有端口限制也必须通过智能检测才能执行
if metadata.Category == "web" {
return false
}
return true return true
} }
// shouldIncludeWebPlugin 判断是否应该包含Web插件智能检测 // shouldIncludeWebPlugin 判断是否应该包含Web插件智能检测
func (b *BaseScanStrategy) shouldIncludeWebPlugin(metadata *base.PluginMetadata, targetPort int) bool { func (b *BaseScanStrategy) shouldIncludeWebPlugin(metadata *base.PluginMetadata, targetHost string, targetPort int) bool {
// 只对服务扫描策略启用Web插件智能检测 // 只对服务扫描策略启用Web插件智能检测
if b.filterType != FilterService { if b.filterType != FilterService {
return false return false
@ -187,14 +194,14 @@ func (b *BaseScanStrategy) shouldIncludeWebPlugin(metadata *base.PluginMetadata,
globalWebDetector = NewWebPortDetector() globalWebDetector = NewWebPortDetector()
} }
// 检测是否为Web服务这里暂时只检查常见端口避免每次都进行HTTP探测) // 检测是否为Web服务使用完整的智能检测包括HTTP协议探测)
return globalWebDetector.IsCommonWebPort(targetPort) return globalWebDetector.IsWebService(targetHost, targetPort)
} }
// globalWebDetector Web检测器全局实例 // globalWebDetector Web检测器全局实例
var globalWebDetector *WebPortDetector var globalWebDetector *WebPortDetector
// IsPluginApplicable 判断插件是否适用(通用实现 // IsPluginApplicable 判断插件是否适用(通用实现,传统插件系统
func (b *BaseScanStrategy) IsPluginApplicable(plugin common.ScanPlugin, targetPort int, isCustomMode bool) bool { func (b *BaseScanStrategy) IsPluginApplicable(plugin common.ScanPlugin, targetPort int, isCustomMode bool) bool {
// 自定义模式下运行所有明确指定的插件 // 自定义模式下运行所有明确指定的插件
if isCustomMode { if isCustomMode {
@ -206,6 +213,21 @@ func (b *BaseScanStrategy) IsPluginApplicable(plugin common.ScanPlugin, targetPo
return false return false
} }
// 对于服务扫描中的Web插件需要进行智能检测传统插件系统
if b.filterType == FilterService && plugin.HasType(common.PluginTypeWeb) {
// 获取Web检测器实例延迟初始化
if globalWebDetector == nil {
globalWebDetector = NewWebPortDetector()
}
// 注意传统插件系统无法传递host参数使用空字符串
// 这是传统插件系统的限制,新插件系统已经解决了这个问题
if targetPort > 0 {
return globalWebDetector.IsWebService("", targetPort)
}
return false
}
// 对于服务扫描,还需检查端口匹配 // 对于服务扫描,还需检查端口匹配
if b.filterType == FilterService { if b.filterType == FilterService {
// 无端口限制的插件适用于所有端口 // 无端口限制的插件适用于所有端口

View File

@ -19,7 +19,7 @@ type ScanStrategy interface {
// 插件管理方法 // 插件管理方法
GetPlugins() ([]string, bool) GetPlugins() ([]string, bool)
IsPluginApplicable(plugin common.ScanPlugin, targetPort int, isCustomMode bool) bool IsPluginApplicable(plugin common.ScanPlugin, targetPort int, isCustomMode bool) bool
IsPluginApplicableByName(pluginName string, targetPort int, isCustomMode bool) bool IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool) bool
} }
// selectStrategy 根据扫描配置选择适当的扫描策略 // selectStrategy 根据扫描配置选择适当的扫描策略
@ -125,7 +125,7 @@ func ExecuteScanTasks(targets []common.HostInfo, strategy ScanStrategy, ch *chan
} }
// 检查插件是否适用于当前目标 // 检查插件是否适用于当前目标
if strategy.IsPluginApplicableByName(pluginName, targetPort, isCustomMode) { if strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode) {
executeScanTask(pluginName, target, ch, wg) executeScanTask(pluginName, target, ch, wg)
} }
} }
@ -143,7 +143,7 @@ func countApplicableTasks(targets []common.HostInfo, pluginsToRun []string, isCu
for _, pluginName := range pluginsToRun { for _, pluginName := range pluginsToRun {
if GlobalPluginAdapter.PluginExists(pluginName) && if GlobalPluginAdapter.PluginExists(pluginName) &&
strategy.IsPluginApplicableByName(pluginName, targetPort, isCustomMode) { strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode) {
count++ count++
} }
} }

View File

@ -1,7 +1,17 @@
package core package core
import ( import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"regexp"
"strconv"
"strings"
"time" "time"
"github.com/shadow1ng/fscan/common"
) )
// WebPortDetector Web端口检测器 // WebPortDetector Web端口检测器
@ -10,49 +20,336 @@ type WebPortDetector struct {
commonWebPorts map[int]bool commonWebPorts map[int]bool
// HTTP检测超时时间 // HTTP检测超时时间
httpTimeout time.Duration httpTimeout time.Duration
// HTTP客户端
httpClient *http.Client
// HTTPS客户端
httpsClient *http.Client
} }
// NewWebPortDetector 创建Web端口检测器 // NewWebPortDetector 创建Web端口检测器
func NewWebPortDetector() *WebPortDetector { func NewWebPortDetector() *WebPortDetector {
// 定义常见Web端口 timeout := 3 * time.Second
// 定义常见Web端口扩展列表
commonPorts := map[int]bool{ commonPorts := map[int]bool{
// 标准Web端口
80: true, // HTTP 80: true, // HTTP
443: true, // HTTPS 443: true, // HTTPS
8080: true, // HTTP alternate 8080: true, // HTTP alternate
8443: true, // HTTPS alternate 8443: true, // HTTPS alternate
// 开发服务器端口
3000: true, // Node.js dev server
4000: true, // Ruby dev server
5000: true, // Python dev server
8000: true, // Development server 8000: true, // Development server
8888: true, // Common dev port 8888: true, // Common dev port
9000: true, // Common dev port 9000: true, // Common dev port
9090: true, // Common dev port 9090: true, // Common dev port
3000: true, // Node.js dev server
4000: true, // Ruby dev server // Web服务器alternate端口
5000: true, // Python dev server 8081: true, 8082: true, 8083: true, 8084: true, 8085: true,
8081: true, // HTTP alternate 8086: true, 8087: true, 8088: true, 8089: true,
8082: true, // HTTP alternate
8083: true, // HTTP alternate // 企业级Web端口
8084: true, // HTTP alternate 9080: true, // WebSphere
8085: true, // HTTP alternate 9443: true, // WebSphere SSL
8086: true, // HTTP alternate 7001: true, // WebLogic
8087: true, // HTTP alternate 7002: true, // WebLogic SSL
8088: true, // HTTP alternate
8089: true, // HTTP alternate // 应用服务器端口
8180: true, // Tomcat alternate
8181: true, // Tomcat alternate
8282: true, // Common alternate
8383: true, // Common alternate
8484: true, // Common alternate
8585: true, // Common alternate
// 代理和负载均衡端口
3128: true, // Squid proxy
8008: true, // HTTP proxy
8118: true, // Privoxy
// 监控和管理界面
9200: true, // Elasticsearch
5601: true, // Kibana
3001: true, // Grafana alternate
9091: true, // Prometheus
}
// 创建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{ return &WebPortDetector{
commonWebPorts: commonPorts, commonWebPorts: commonPorts,
httpTimeout: 2 * time.Second, // 2秒超时快速检测 httpTimeout: timeout,
httpClient: httpClient,
httpsClient: httpsClient,
} }
} }
// 已移除未使用的 IsWebService 方法 // 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. 智能路径对非常见端口进行HTTP协议探测
common.LogDebug(fmt.Sprintf("对端口 %d 进行智能Web检测", port))
result := w.detectHTTPService(host, port)
common.LogDebug(fmt.Sprintf("端口 %d 智能检测结果: %v", port, result))
return result
}
// IsCommonWebPort 检查是否为常见Web端口 // IsCommonWebPort 检查是否为常见Web端口
func (w *WebPortDetector) IsCommonWebPort(port int) bool { func (w *WebPortDetector) IsCommonWebPort(port int) bool {
return w.commonWebPorts[port] return w.commonWebPorts[port]
} }
// 已移除未使用的 detectHTTPService 方法 // detectHTTPService 检测HTTP服务真正的智能检测
func (w *WebPortDetector) detectHTTPService(host string, port int) bool {
// 创建检测上下文,避免长时间阻塞
ctx, cancel := context.WithTimeout(context.Background(), w.httpTimeout)
defer cancel()
// 已移除未使用的 tryHTTPConnection 方法 // 并发检测HTTP和HTTPS
httpChan := make(chan bool, 1)
httpsChan := make(chan bool, 1)
// 已移除未使用的 GetCommonWebPorts 方法 // 检测HTTP
go func() {
httpChan <- w.tryHTTPConnection(ctx, host, port, "http")
}()
// 检测HTTPS对于高端口常见
go func() {
httpsChan <- w.tryHTTPConnection(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 {
// 构造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())
// 先检查明确的非Web服务错误
nonWebErrors := []string{
"connection refused",
"no such host",
"network unreachable",
"timeout",
"deadline exceeded",
"connection reset",
"eof",
}
for _, nonWebErr := range nonWebErrors {
if strings.Contains(errStr, nonWebErr) {
return false
}
}
// 这些错误表明连接到了HTTP服务器只是协议或其他问题
webIndicators := []string{
"malformed http response",
"unexpected http response",
"ssl handshake",
"certificate",
"tls",
"bad request",
"method not allowed",
"server gave http response",
}
for _, indicator := range webIndicators {
if strings.Contains(errStr, indicator) {
return true
}
}
return false
}
// analyzeHTTPResponse 分析HTTP响应判断是否为Web服务
func (w *WebPortDetector) analyzeHTTPResponse(resp *http.Response, url string) bool {
// 任何HTTP状态码都表明这是Web服务
if resp.StatusCode > 0 {
// 分析响应头获取更多信息
serverHeader := resp.Header.Get("Server")
contentType := resp.Header.Get("Content-Type")
// 记录检测到的Web服务器信息
if serverHeader != "" {
common.LogDebug(fmt.Sprintf("检测到Web服务器: %s (Server: %s)", url, serverHeader))
}
// 特殊处理某些非Web服务也会返回HTTP响应
if w.isNonWebHTTPService(serverHeader, contentType) {
common.LogDebug(fmt.Sprintf("端口返回HTTP响应但可能不是Web服务: %s", url))
return false
}
return true
}
return false
}
// isNonWebHTTPService 判断是否为非Web的HTTP服务
func (w *WebPortDetector) isNonWebHTTPService(serverHeader, contentType string) bool {
// 某些服务使用HTTP协议但不是传统Web服务
nonWebServices := []string{
"docker",
"kubernetes",
"consul",
"etcd",
"vault",
"nomad",
}
serverLower := strings.ToLower(serverHeader)
for _, service := range nonWebServices {
if strings.Contains(serverLower, service) {
return true
}
}
return false
}
// GetWebPortRange 获取可能的Web端口范围用于高级扫描
func (w *WebPortDetector) GetWebPortRange() []int {
// 返回扩展的Web端口列表用于目标发现
var ports []int
for port := range w.commonWebPorts {
ports = append(ports, port)
}
// 添加一些常见的自定义端口范围
customRanges := []int{
8090, 8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099,
9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009,
10000, 10001, 10002, 10003, 10004, 10005,
}
ports = append(ports, customRanges...)
return ports
}
// IsWebPortByPattern 基于端口模式判断是否可能是Web端口
func (w *WebPortDetector) IsWebPortByPattern(port int) bool {
portStr := strconv.Itoa(port)
// Web端口的常见模式
webPatterns := []*regexp.Regexp{
regexp.MustCompile(`^80\d{2}$`), // 80xx
regexp.MustCompile(`^90\d{2}$`), // 90xx
regexp.MustCompile(`^300\d$`), // 300x
regexp.MustCompile(`^[3-9]000$`), // x000
regexp.MustCompile(`^1[0-9]000$`), // 1x000
}
for _, pattern := range webPatterns {
if pattern.MatchString(portStr) {
return true
}
}
return false
}