package core import ( "context" "crypto/tls" "fmt" "net" "net/http" "net/url" "strconv" "strings" "sync" "github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/common/i18n" gmtls "github.com/tjfoc/gmsm/gmtls" ) // WebPortDetector 简化的Web检测器 - 保持API兼容 type WebPortDetector struct{} // GetWebPortDetector 获取检测器实例 - 保持API兼容,删除单例模式 func GetWebPortDetector() *WebPortDetector { return &WebPortDetector{} } // DetectHTTPScheme 智能检测HTTP/HTTPS协议 // 策略:TLS握手优先(快速且准确),失败后尝试GM TLS,最后HTTP // 返回: "https", "https-gm", "http", 或 "" (都不是Web服务) func DetectHTTPScheme(host string, port int, config *common.Config, session *common.ScanSession) string { return DetectHTTPSchemeContext(context.Background(), host, port, config, session) } func DetectHTTPSchemeContext(ctx context.Context, host string, port int, config *common.Config, session *common.ScanSession) string { // 优化:先快速检测 TCP 连通性 if !isPortReachable(ctx, host, port, config, session) { return "" } timeout := config.Network.WebTimeout addr := net.JoinHostPort(host, strconv.Itoa(port)) // 第一步:尝试标准TLS握手(优先检测HTTPS) tlsDialer := &net.Dialer{Timeout: timeout} tlsConn, err := tls.DialWithDialer( tlsDialer, "tcp", addr, &tls.Config{ InsecureSkipVerify: true, MinVersion: tls.VersionTLS10, }, ) if err == nil { _ = tlsConn.Close() return "https" } // 第二步:仅在标准 TLS 握手级别失败(cipher/protocol 不兼容)时尝试国密 // 连接级别失败(timeout/refused/非 TLS 端口)不需要尝试 if maybeGMTLS(err) { gmConn, gmErr := gmtls.DialWithDialer( tlsDialer, "tcp", addr, &gmtls.Config{ GMSupport: gmtls.NewGMSupport(), InsecureSkipVerify: true, }, ) if gmErr == nil { _ = gmConn.Close() return "https-gm" } } // TLS和GM TLS都失败,尝试HTTP client := createHTTPClient(config, session) // 使用HEAD请求(更轻量) httpURL := fmt.Sprintf("http://%s", addr) req, err := http.NewRequestWithContext(ctx, "HEAD", httpURL, nil) if err != nil { return "" } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") req.Header.Set("Accept", "*/*") resp, err := session.HTTPDo(client, req) if err == nil { _ = resp.Body.Close() return "http" } // HTTP也失败,记录并返回空 return "" } // createHTTPClient 创建统一的HTTP客户端 - 支持HTTP/HTTPS和代理 func createHTTPClient(config *common.Config, session *common.ScanSession) *http.Client { timeout := config.Network.WebTimeout // 创建基础Transport,配置连接和 TLS 超时 transport := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, DisableKeepAlives: true, // 设置连接超时,避免长时间等待无响应的服务器 DialContext: (&net.Dialer{ Timeout: timeout, }).DialContext, // TLS 握手超时 TLSHandshakeTimeout: timeout, } // 配置代理设置 networkConfig := config.Network if networkConfig.HTTPProxy != "" { // 使用HTTP代理 httpProxy := networkConfig.HTTPProxy if !strings.Contains(httpProxy, "://") { httpProxy = "http://" + httpProxy } if proxyURL, err := url.Parse(httpProxy); err == nil && proxyURL.Host != "" { transport.Proxy = http.ProxyURL(proxyURL) } else { session.LogError(i18n.Tr("http_proxy_config_error", err)) } } else if networkConfig.Socks5Proxy != "" { // 使用SOCKS5代理 - 需要特殊处理 if _, err := url.Parse(networkConfig.Socks5Proxy); err == nil { // SOCKS5代理需要使用代理管理器 // 这里先记录警告,建议使用HTTP代理进行Web检测 session.LogError(i18n.GetText("socks5_not_supported_web")) } } return &http.Client{ Timeout: timeout, Transport: transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse // 不跟随重定向 }, } } // DetectHTTPServiceOnly HTTP协议检测 - 保持API兼容,简化实现 func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *common.Config, session *common.ScanSession) bool { return w.DetectHTTPServiceOnlyContext(context.Background(), host, port, config, session) } func (w *WebPortDetector) DetectHTTPServiceOnlyContext(ctx context.Context, host string, port int, config *common.Config, session *common.ScanSession) bool { // 优化:先快速检测 TCP 连通性,避免在不可达端口上浪费双倍超时时间 // 对于不存在的端口,这可以将检测时间从 2×timeout 减少到 1×timeout if !isPortReachable(ctx, host, port, config, session) { return false } client := createHTTPClient(config, session) // 尝试HTTP if w.tryHTTP(ctx, client, session, host, port, "http") { return true } // 尝试HTTPS if w.tryHTTP(ctx, client, session, host, port, "https") { return true } return false } // isPortReachable 快速检测端口是否可达(TCP 连接测试) // 用于在 HTTP/HTTPS 检测前过滤不可达端口,避免双重超时 func isPortReachable(ctx context.Context, host string, port int, config *common.Config, session *common.ScanSession) bool { timeout := config.Network.WebTimeout addr := net.JoinHostPort(host, strconv.Itoa(port)) conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return false } _ = conn.Close() return true } // tryHTTP 尝试HTTP请求 - 简化的核心逻辑 func (w *WebPortDetector) tryHTTP(ctx context.Context, client *http.Client, session *common.ScanSession, host string, port int, protocol string) bool { // 构造URL targetURL := (&url.URL{Scheme: protocol, Host: net.JoinHostPort(host, strconv.Itoa(port))}).String() // 发送HEAD请求 req, err := http.NewRequestWithContext(ctx, "HEAD", targetURL, nil) if err != nil { return false } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") req.Header.Set("Accept", "*/*") resp, err := session.HTTPDo(client, req) if err != nil { return false } defer func() { _ = resp.Body.Close() }() // 简单有效的判断:有HTTP状态码就是Web服务 return resp.StatusCode > 0 && resp.StatusCode < 600 } // =============================== // 基于服务指纹的Web服务识别 // =============================== // globalState 全局 State 兼容指针(向后兼容不接受 State 的旧调用方) // 新代码应通过 State 方法访问服务缓存 var globalState *common.State // IsWebServiceByFingerprint 基于服务指纹判断Web服务 - 保持API兼容 // 服务识别规则 - 编译期常量,避免运行时分配 var ( nonWebKeywords = []string{ "oracle", "mysql", "postgresql", "redis", "mongodb", "ssh", "telnet", "ftp", "smtp", "pop3", "imap", "ldap", "snmp", "vnc", "rdp", "smb", } webKeywords = []string{ "http", "https", "nginx", "apache", "iis", "tomcat", "jetty", "nodejs", "php", "asp", "jsp", } bannerKeywords = []string{"server:", "http/", "content-type:"} ) // IsWebServiceByFingerprint 通过指纹判断是否为Web服务 func IsWebServiceByFingerprint(serviceInfo *ServiceInfo) bool { if serviceInfo == nil || serviceInfo.Name == "" { return false } serviceName := strings.ToLower(serviceInfo.Name) // 非Web服务优先检查(短路) for _, keyword := range nonWebKeywords { if strings.Contains(serviceName, keyword) { return false } } // Web服务名检查 for _, keyword := range webKeywords { if strings.Contains(serviceName, keyword) { return true } } // Banner特征检查 if serviceInfo.Banner != "" { banner := strings.ToLower(serviceInfo.Banner) for _, keyword := range bannerKeywords { if strings.Contains(banner, keyword) { return true } } } return false } // isDefinitelyNonWeb 判断服务是否明确不是 Web 服务 // 只检查 nonWebKeywords,不在里面 = 不确定 = 值得做 HTTP 探测 func isDefinitelyNonWeb(serviceInfo *ServiceInfo) bool { if serviceInfo == nil || serviceInfo.Name == "" { return false } serviceName := strings.ToLower(serviceInfo.Name) for _, keyword := range nonWebKeywords { if strings.Contains(serviceName, keyword) { return true } } return false } // SetGlobalState 设置全局 State(RunScan 入口调用,兼容旧代码路径) func SetGlobalState(state *common.State) { globalState = state } func resolveState(state *common.State) *common.State { if state != nil { return state } return globalState } // CacheServiceInfoWithState 缓存服务信息到指定 State func CacheServiceInfoWithState(state *common.State, host string, port int, serviceInfo *ServiceInfo) { s := resolveState(state) if s == nil { return } key := net.JoinHostPort(host, strconv.Itoa(port)) s.CacheService(key, serviceInfo) } // CacheServiceInfo 兼容旧调用(使用全局 State) func CacheServiceInfo(host string, port int, serviceInfo *ServiceInfo) { CacheServiceInfoWithState(nil, host, port, serviceInfo) } // MarkAsWebService 标记 Web 服务 func MarkAsWebService(host string, port int, serviceInfo *ServiceInfo) { CacheServiceInfo(host, port, serviceInfo) } // GetCachedServiceInfoWithState 从指定 State 获取缓存的服务信息 func GetCachedServiceInfoWithState(state *common.State, host string, port int) (*ServiceInfo, bool) { s := resolveState(state) if s == nil { return nil, false } key := net.JoinHostPort(host, strconv.Itoa(port)) val, ok := s.GetCachedService(key) if !ok { return nil, false } info, ok := val.(*ServiceInfo) return info, ok } // GetCachedServiceInfo 兼容旧调用 func GetCachedServiceInfo(host string, port int) (*ServiceInfo, bool) { return GetCachedServiceInfoWithState(nil, host, port) } // GetWebServiceInfo 获取 Web 服务信息 func GetWebServiceInfo(host string, port int) (*ServiceInfo, bool) { return GetWebServiceInfoWithState(nil, host, port) } // GetWebServiceInfoWithState 从指定 State 获取 Web 服务信息 func GetWebServiceInfoWithState(state *common.State, host string, port int) (*ServiceInfo, bool) { info, exists := GetCachedServiceInfoWithState(state, host, port) if !exists || !IsWebServiceByFingerprint(info) { return nil, false } return info, true } // IsMarkedWebService 检查是否为 Web 服务(使用全局 State) func IsMarkedWebService(host string, port int) bool { _, exists := GetWebServiceInfo(host, port) return exists } // IsMarkedWebServiceWithState 检查是否为 Web 服务(指定 State) func IsMarkedWebServiceWithState(state *common.State, host string, port int) bool { _, exists := GetWebServiceInfoWithState(state, host, port) return exists } // =============================== // Web扫描策略 // =============================== // WebScanStrategy Web扫描策略 type WebScanStrategy struct { *BaseScanStrategy } // NewWebScanStrategy 创建新的Web扫描策略 func NewWebScanStrategy() *WebScanStrategy { return &WebScanStrategy{ BaseScanStrategy: NewBaseScanStrategy(i18n.GetText("scan_strategy_web_name"), FilterWeb), } } // Name 返回策略名称 func (s *WebScanStrategy) Name() string { return i18n.GetText("scan_strategy_web_name") } // Description 返回策略描述 func (s *WebScanStrategy) Description() string { return i18n.GetText("scan_strategy_web_desc") } // Execute 执行Web扫描策略 func (s *WebScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { // 输出扫描开始信息 s.LogScanStart(session) // 验证插件配置 if err := s.ValidateConfiguration(); err != nil { session.LogError(err.Error()) return } // 准备URL目标 targets := s.prepareTargets(info, session.State, session) // 输出插件信息 s.LogPluginInfo(session.Config, session) // 执行扫描任务 ExecuteScanTasks(ctx, session, targets, s, ch, wg) } // PrepareTargets 准备URL目标列表 func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo, state *common.State) []common.HostInfo { return s.prepareTargets(baseInfo, state, nil) } func (s *WebScanStrategy) prepareTargets(baseInfo common.HostInfo, state *common.State, session *common.ScanSession) []common.HostInfo { var targetInfos []common.HostInfo // 首先从State获取URL目标 urls := state.GetURLs() for _, urlStr := range urls { urlInfo := s.createTargetFromURLWithSession(baseInfo, urlStr, session) if urlInfo != nil { targetInfos = append(targetInfos, *urlInfo) } } // 如果URLs为空但baseInfo.Url有值,使用baseInfo.URL if len(targetInfos) == 0 && baseInfo.URL != "" { urlInfo := s.createTargetFromURLWithSession(baseInfo, baseInfo.URL, session) if urlInfo != nil { targetInfos = append(targetInfos, *urlInfo) } } return targetInfos } // createTargetFromURL 从URL创建目标信息 func (s *WebScanStrategy) createTargetFromURL(baseInfo common.HostInfo, urlStr string) *common.HostInfo { return s.createTargetFromURLWithSession(baseInfo, urlStr, nil) } func (s *WebScanStrategy) createTargetFromURLWithSession(baseInfo common.HostInfo, urlStr string, session *common.ScanSession) *common.HostInfo { // 确保URL包含协议头 if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") { urlStr = "http://" + urlStr } // 解析URL获取Host和Port信息 parsedURL, err := url.Parse(urlStr) if err != nil { if session != nil { session.LogError(i18n.Tr("url_parse_failed", urlStr, err)) } return nil } urlInfo := baseInfo urlInfo.URL = urlStr urlInfo.Host = parsedURL.Hostname() if urlInfo.Host == "" { if session != nil { session.LogError(i18n.Tr("url_parse_failed", urlStr, "empty host")) } return nil } // 设置端口 portStr := parsedURL.Port() if portStr == "" { if hasMalformedURLPort(parsedURL.Host) { if session != nil { session.LogError(i18n.Tr("host_port_invalid", urlInfo.Host, "")) } return nil } // 根据协议设置默认端口 if parsedURL.Scheme == "https" { urlInfo.Port = 443 } else { urlInfo.Port = 80 } } else { port, err := strconv.Atoi(portStr) if err != nil || port < 1 || port > 65535 { if session != nil { session.LogError(i18n.Tr("host_port_invalid", urlInfo.Host, portStr)) } return nil } urlInfo.Port = port } // 标记为Web服务,确保Web插件能识别此目标 MarkAsWebService(urlInfo.Host, urlInfo.Port, &ServiceInfo{Name: "http"}) return &urlInfo } // maybeGMTLS 判断标准 TLS 握手错误是否可能是国密服务端 // 只有 cipher/protocol 层面的不兼容才值得尝试国密回退 // 连接超时、拒绝、非 TLS 端口等连接级错误直接跳过 func maybeGMTLS(err error) bool { if err == nil { return false } s := err.Error() return strings.Contains(s, "handshake failure") || strings.Contains(s, "protocol version") || strings.Contains(s, "no mutual") || strings.Contains(s, "cipher suite") } func hasMalformedURLPort(host string) bool { if strings.HasPrefix(host, "[") { end := strings.LastIndexByte(host, ']') return end >= 0 && len(host) > end+1 && host[end+1] == ':' } return strings.Contains(host, ":") }