Files
fscan/core/web_scanner.go
T
ZacharyZcR 1595c92aed
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
feat: 统一服务缓存 + 指纹驱动插件匹配
将 webServiceCache 扩展为通用 serviceCache,所有指纹识别结果
统一缓存,插件匹配时端口不命中则回退到服务名称匹配。

删除多余的 service_cache.go,复用已有的 ServiceInfo 体系。
补充 nil 防御、Explicit 标记、大量单元/集成/回归测试。
2026-06-12 19:49:07 +08:00

433 lines
12 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代理
if proxyURL, err := url.Parse(networkConfig.HTTPProxy); err == nil {
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服务识别
// ===============================
// 服务识别缓存 - 存储所有识别到的服务(不仅限于 Web)
// 端口扫描阶段写入,插件匹配阶段读取
var (
serviceCache = make(map[string]*ServiceInfo)
serviceCacheMutex sync.RWMutex
)
// 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
}
// CacheServiceInfo 缓存识别到的服务信息
func CacheServiceInfo(host string, port int, serviceInfo *ServiceInfo) {
cacheKey := net.JoinHostPort(host, strconv.Itoa(port))
serviceCacheMutex.Lock()
defer serviceCacheMutex.Unlock()
serviceCache[cacheKey] = serviceInfo
}
// MarkAsWebService 标记 Web 服务(兼容旧调用)
func MarkAsWebService(host string, port int, serviceInfo *ServiceInfo) {
CacheServiceInfo(host, port, serviceInfo)
}
// GetCachedServiceInfo 获取缓存的服务信息
func GetCachedServiceInfo(host string, port int) (*ServiceInfo, bool) {
cacheKey := net.JoinHostPort(host, strconv.Itoa(port))
serviceCacheMutex.RLock()
defer serviceCacheMutex.RUnlock()
serviceInfo, exists := serviceCache[cacheKey]
return serviceInfo, exists
}
// GetWebServiceInfo 获取 Web 服务信息(兼容旧调用)
func GetWebServiceInfo(host string, port int) (*ServiceInfo, bool) {
info, exists := GetCachedServiceInfo(host, port)
if !exists {
return nil, false
}
if !IsWebServiceByFingerprint(info) {
return nil, false
}
return info, true
}
// IsMarkedWebService 检查是否为 Web 服务
func IsMarkedWebService(host string, port int) bool {
_, exists := GetWebServiceInfo(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 {
session.LogError(i18n.Tr("url_parse_failed", urlStr, err))
return nil
}
urlInfo := baseInfo
urlInfo.URL = urlStr
urlInfo.Host = parsedURL.Hostname()
// 设置端口
portStr := parsedURL.Port()
if portStr == "" {
// 根据协议设置默认端口
if parsedURL.Scheme == "https" {
urlInfo.Port = 443
} else {
urlInfo.Port = 80
}
} else {
// 解析端口字符串为整数
var port int
if _, err := fmt.Sscanf(portStr, "%d", &port); err == nil {
urlInfo.Port = port
} else {
// 解析失败时使用默认端口
if parsedURL.Scheme == "https" {
urlInfo.Port = 443
} else {
urlInfo.Port = 80
}
}
}
// 标记为Web服务,确保Web插件能识别此目标
MarkAsWebService(urlInfo.Host, urlInfo.Port, &ServiceInfo{Name: "http"})
return &urlInfo
}