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