Files
fscan/core/web_scanner.go
T
ZacharyZcR 02ad8f5334 refactor: 4项架构优化 — CEL缓存/POC隔离/服务缓存/结果统一
1. CEL 表达式编译缓存
   - 新增 CelProgCache,同一 POC 的所有规则/参数组合共享编译后的 Program
   - clusterpoc 热路径上消除重复的 Compile+Program 调用

2. POC 全局状态消除
   - allPocs/pocLoaded 全局变量改为 pocStore 按 PocPath 缓存
   - 不同 PocPath 的扫描独立加载,Web API 并发场景不再互相覆盖

3. serviceCache 下沉到 per-session State
   - 服务识别缓存从包级全局 map 迁移到 State.serviceCache (sync.Map)
   - BaseScanStrategy 通过 SetState 注入 session state
   - 消除多个并发扫描之间的服务识别缓存串台

4. POC 结果输出路径统一
   - 提取 buildVulnDetails/buildVulnLogMsg/saveVulnResult 三个公共函数
   - CheckMultiPoc 和 recordVulnerabilityResult 共用统一的结果构造逻辑
   - 消除 details 字段名不一致和日志格式差异
2026-06-13 19:24:57 +08:00

501 lines
14 KiB
Go
Raw 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"
"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握手(GM TLS fallback
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", "ssl", "tls", "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 设置全局 StateRunScan 入口调用,兼容旧代码路径)
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
}
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, ":")
}