diff --git a/core/BaseScanStrategy.go b/core/BaseScanStrategy.go index 142ec38..96d534b 100644 --- a/core/BaseScanStrategy.go +++ b/core/BaseScanStrategy.go @@ -108,7 +108,8 @@ func (b *BaseScanStrategy) isPluginTypeMatchedByName(pluginName string) bool { case FilterLocal: return metadata.Category == "local" case FilterService: - return metadata.Category == "service" + // 服务扫描允许service类型,以及通过智能检测的web类型(在上层逻辑中处理) + return metadata.Category == "service" || metadata.Category == "web" case FilterWeb: return metadata.Category == "web" default: @@ -122,6 +123,7 @@ func (b *BaseScanStrategy) isPluginTypeMatched(plugin common.ScanPlugin) bool { case FilterLocal: return plugin.HasType(common.PluginTypeLocal) case FilterService: + // 服务扫描排除本地插件,允许服务和Web插件 return !plugin.HasType(common.PluginTypeLocal) case FilterWeb: return plugin.HasType(common.PluginTypeWeb) @@ -131,7 +133,7 @@ func (b *BaseScanStrategy) isPluginTypeMatched(plugin common.ScanPlugin) bool { } // 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 { return true @@ -143,7 +145,7 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetPor } // 智能Web插件检测:如果是Web插件且检测到Web服务,则包含Web插件 - if b.shouldIncludeWebPlugin(metadata, targetPort) { + if b.shouldIncludeWebPlugin(metadata, targetHost, targetPort) { return true } @@ -162,11 +164,16 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetPor return false } + // 对于Web插件,即使没有端口限制,也必须通过智能检测才能执行 + if metadata.Category == "web" { + return false + } + return true } // 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插件智能检测 if b.filterType != FilterService { return false @@ -187,14 +194,14 @@ func (b *BaseScanStrategy) shouldIncludeWebPlugin(metadata *base.PluginMetadata, globalWebDetector = NewWebPortDetector() } - // 检测是否为Web服务(这里暂时只检查常见端口,避免每次都进行HTTP探测) - return globalWebDetector.IsCommonWebPort(targetPort) + // 检测是否为Web服务(使用完整的智能检测,包括HTTP协议探测) + return globalWebDetector.IsWebService(targetHost, targetPort) } // globalWebDetector Web检测器全局实例 var globalWebDetector *WebPortDetector -// IsPluginApplicable 判断插件是否适用(通用实现) +// IsPluginApplicable 判断插件是否适用(通用实现,传统插件系统) func (b *BaseScanStrategy) IsPluginApplicable(plugin common.ScanPlugin, targetPort int, isCustomMode bool) bool { // 自定义模式下运行所有明确指定的插件 if isCustomMode { @@ -206,6 +213,21 @@ func (b *BaseScanStrategy) IsPluginApplicable(plugin common.ScanPlugin, targetPo 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 { // 无端口限制的插件适用于所有端口 diff --git a/core/Scanner.go b/core/Scanner.go index eccd3e6..1ac71b1 100644 --- a/core/Scanner.go +++ b/core/Scanner.go @@ -19,7 +19,7 @@ type ScanStrategy interface { // 插件管理方法 GetPlugins() ([]string, 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 根据扫描配置选择适当的扫描策略 @@ -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) } } @@ -143,7 +143,7 @@ func countApplicableTasks(targets []common.HostInfo, pluginsToRun []string, isCu for _, pluginName := range pluginsToRun { if GlobalPluginAdapter.PluginExists(pluginName) && - strategy.IsPluginApplicableByName(pluginName, targetPort, isCustomMode) { + strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode) { count++ } } diff --git a/core/WebDetection.go b/core/WebDetection.go index 76f2472..464d6d9 100644 --- a/core/WebDetection.go +++ b/core/WebDetection.go @@ -1,7 +1,17 @@ package core import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "regexp" + "strconv" + "strings" "time" + + "github.com/shadow1ng/fscan/common" ) // WebPortDetector Web端口检测器 @@ -10,49 +20,336 @@ type WebPortDetector struct { commonWebPorts map[int]bool // HTTP检测超时时间 httpTimeout time.Duration + // HTTP客户端 + httpClient *http.Client + // HTTPS客户端 + httpsClient *http.Client } // NewWebPortDetector 创建Web端口检测器 func NewWebPortDetector() *WebPortDetector { - // 定义常见Web端口 + timeout := 3 * time.Second + + // 定义常见Web端口(扩展列表) commonPorts := map[int]bool{ + // 标准Web端口 80: true, // HTTP 443: true, // HTTPS 8080: true, // HTTP 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 8888: true, // Common dev port 9000: true, // Common dev port 9090: true, // Common dev port - 3000: true, // Node.js dev server - 4000: true, // Ruby dev server - 5000: true, // Python dev server - 8081: true, // HTTP alternate - 8082: true, // HTTP alternate - 8083: true, // HTTP alternate - 8084: true, // HTTP alternate - 8085: true, // HTTP alternate - 8086: true, // HTTP alternate - 8087: true, // HTTP alternate - 8088: true, // HTTP alternate - 8089: true, // HTTP alternate + + // Web服务器alternate端口 + 8081: true, 8082: true, 8083: true, 8084: true, 8085: true, + 8086: true, 8087: true, 8088: true, 8089: true, + + // 企业级Web端口 + 9080: true, // WebSphere + 9443: true, // WebSphere SSL + 7001: true, // WebLogic + 7002: true, // WebLogic SSL + + // 应用服务器端口 + 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{ 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端口 func (w *WebPortDetector) IsCommonWebPort(port int) bool { 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 方法 \ No newline at end of file + // 检测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 +} \ No newline at end of file