diff --git a/core/port_scan.go b/core/port_scan.go index 3fbf7a6..169c79c 100644 --- a/core/port_scan.go +++ b/core/port_scan.go @@ -504,7 +504,7 @@ func scanSinglePort(ctx context.Context, host string, port int, addr string, ada serviceInfo, _ := scanner.SmartIdentify() // 步骤4:处理结果 - processServiceResult(host, port, addr, serviceInfo, config, session) + processServiceResult(ctx, host, port, addr, serviceInfo, config, session) } // handleConnectionFailure 处理连接失败 @@ -651,10 +651,10 @@ func saveOpenPort(session *common.ScanSession, host string, port int) { } // processServiceResult 处理服务识别结果 -func processServiceResult(host string, port int, addr string, serviceInfo *ServiceInfo, config *common.Config, session *common.ScanSession) { +func processServiceResult(ctx context.Context, host string, port int, addr string, serviceInfo *ServiceInfo, config *common.Config, session *common.ScanSession) { if serviceInfo == nil { // 服务识别失败,尝试 HTTP 回退探测 - if !tryHTTPFallbackDetection(host, port, addr, config, session) { + if !tryHTTPFallbackDetection(ctx, host, port, addr, config, session) { session.LogInfo(i18n.Tr("port_open", addr)) } return @@ -714,10 +714,10 @@ func buildServiceDetails(port int, info *ServiceInfo) map[string]interface{} { } // tryHTTPFallbackDetection 尝试HTTP回退探测,返回是否成功识别为HTTP服务 -func tryHTTPFallbackDetection(host string, port int, addr string, config *common.Config, session *common.ScanSession) bool { +func tryHTTPFallbackDetection(ctx context.Context, host string, port int, addr string, config *common.Config, session *common.ScanSession) bool { // 使用WebDetection进行HTTP协议探测 webDetector := GetWebPortDetector() - if !webDetector.DetectHTTPServiceOnly(host, port, config, session) { + if !webDetector.DetectHTTPServiceOnlyContext(ctx, host, port, config, session) { return false } @@ -806,7 +806,7 @@ func probeSubnets(ctx context.Context, hosts []string, timeout time.Duration, se limiter <- struct{}{} go func(pfx, addr string) { defer func() { <-limiter; wg.Done() }() - conn, err := net.DialTimeout("tcp", addr, subnetProbeTimeout) + conn, err := session.DialTCP(ctx, "tcp", addr, subnetProbeTimeout) if err == nil { _ = conn.Close() aliveSubnets.Store(pfx, true) @@ -844,7 +844,7 @@ func probeSubnets(ctx context.Context, hosts []string, timeout time.Duration, se go func(pfx, h string, p int) { defer func() { <-limiter; wg.Done() }() - conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", h, p), subnetProbeTimeout) + conn, err := session.DialTCP(ctx, "tcp", fmt.Sprintf("%s:%d", h, p), subnetProbeTimeout) if err == nil { _ = conn.Close() aliveSubnets.Store(pfx, true) diff --git a/core/web_scanner.go b/core/web_scanner.go index c0dcb51..c34db2c 100644 --- a/core/web_scanner.go +++ b/core/web_scanner.go @@ -28,8 +28,12 @@ func GetWebPortDetector() *WebPortDetector { // 策略: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(host, port, config, session) { + if !isPortReachable(ctx, host, port, config, session) { return "" } @@ -72,7 +76,13 @@ func DetectHTTPScheme(host string, port int, config *common.Config, session *com // 使用HEAD请求(更轻量) httpURL := fmt.Sprintf("http://%s", addr) - resp, err := client.Head(httpURL) + 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" @@ -127,21 +137,25 @@ func createHTTPClient(config *common.Config, session *common.ScanSession) *http. // 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(host, port, config, session) { + if !isPortReachable(ctx, host, port, config, session) { return false } client := createHTTPClient(config, session) // 尝试HTTP - if w.tryHTTP(client, session, host, port, "http") { + if w.tryHTTP(ctx, client, session, host, port, "http") { return true } // 尝试HTTPS - if w.tryHTTP(client, session, host, port, "https") { + if w.tryHTTP(ctx, client, session, host, port, "https") { return true } @@ -150,11 +164,11 @@ func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *c // isPortReachable 快速检测端口是否可达(TCP 连接测试) // 用于在 HTTP/HTTPS 检测前过滤不可达端口,避免双重超时 -func isPortReachable(host string, port int, config *common.Config, session *common.ScanSession) bool { +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(context.Background(), "tcp", addr, timeout) + conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return false } @@ -163,7 +177,7 @@ func isPortReachable(host string, port int, config *common.Config, session *comm } // tryHTTP 尝试HTTP请求 - 简化的核心逻辑 -func (w *WebPortDetector) tryHTTP(client *http.Client, session *common.ScanSession, host string, port int, protocol string) bool { +func (w *WebPortDetector) tryHTTP(ctx context.Context, client *http.Client, session *common.ScanSession, host string, port int, protocol string) bool { // 构造URL var url string if (port == 80 && protocol == "http") || (port == 443 && protocol == "https") { @@ -173,7 +187,7 @@ func (w *WebPortDetector) tryHTTP(client *http.Client, session *common.ScanSessi } // 发送HEAD请求 - req, err := http.NewRequest("HEAD", url, nil) + req, err := http.NewRequestWithContext(ctx, "HEAD", url, nil) if err != nil { return false } diff --git a/core/web_scanner_test.go b/core/web_scanner_test.go index 1597a73..54c52c9 100644 --- a/core/web_scanner_test.go +++ b/core/web_scanner_test.go @@ -1,6 +1,7 @@ package core import ( + "context" "crypto/tls" "fmt" "net" @@ -32,6 +33,26 @@ web_scanner_test.go - WebScanner核心逻辑测试 缓存操作需要验证并发安全性。" */ +func TestDetectHTTPServiceOnlyContextHonorsCancellation(t *testing.T) { + cfg := common.GetGlobalConfig() + oldTimeout := cfg.Network.WebTimeout + cfg.Network.WebTimeout = 2 * time.Second + defer func() { cfg.Network.WebTimeout = oldTimeout }() + + session := common.NewScanSession(cfg, common.NewState(), common.GetFlagVars()) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + start := time.Now() + detected := GetWebPortDetector().DetectHTTPServiceOnlyContext(ctx, "203.0.113.1", 80, cfg, session) + if detected { + t.Fatal("canceled web detection should not report a service") + } + if elapsed := time.Since(start); elapsed > 200*time.Millisecond { + t.Fatalf("canceled web detection took %s", elapsed) + } +} + // ============================================================================= // 核心逻辑测试:Web服务识别 // ============================================================================= diff --git a/plugins/services/elasticsearch.go b/plugins/services/elasticsearch.go index 8dfbb35..c3339af 100644 --- a/plugins/services/elasticsearch.go +++ b/plugins/services/elasticsearch.go @@ -27,15 +27,14 @@ func NewElasticsearchPlugin() *ElasticsearchPlugin { func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { config := session.Config - state := session.State target := info.Target() if config.DisableBrute { - return p.identifyService(ctx, info, config, state) + return p.identifyService(ctx, info, session) } // 首先检测未授权访问 - if p.testCredential(ctx, info, Credential{Username: "", Password: ""}, config, state) { + if p.testCredential(ctx, info, Credential{Username: "", Password: ""}, session) { common.LogVuln(i18n.Tr("elasticsearch_unauth", target)) return &ScanResult{ Success: true, @@ -56,7 +55,7 @@ func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo, s } for _, cred := range credentials { - if p.testCredential(ctx, info, cred, config, state) { + if p.testCredential(ctx, info, cred, session) { common.LogVuln(i18n.Tr("elasticsearch_credential", target, cred.Username, cred.Password)) return &ScanResult{ Success: true, @@ -75,7 +74,8 @@ func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo, s } } -func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) bool { +func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) bool { + config := session.Config client := &http.Client{ Timeout: config.Timeout, Transport: &http.Transport{ @@ -100,12 +100,10 @@ func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.H req.Header.Set("Authorization", "Basic "+auth) } - resp, err := client.Do(req) + resp, err := session.HTTPDo(client, req) if err != nil { - state.IncrementTCPFailedPacketCount() return false } - state.IncrementTCPSuccessPacketCount() defer func() { _ = resp.Body.Close() }() if resp.StatusCode == 200 { @@ -121,15 +119,15 @@ func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.H return false } -func (p *ElasticsearchPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *ElasticsearchPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - if p.testCredential(ctx, info, Credential{Username: "", Password: ""}, config, state) { + if p.testCredential(ctx, info, Credential{Username: "", Password: ""}, session) { banner := "Elasticsearch" common.LogSuccess(i18n.Tr("elasticsearch_service", target, banner)) return &ScanResult{ Success: true, - Type: plugins.ResultTypeService, + Type: plugins.ResultTypeService, Service: "elasticsearch", Banner: banner, } diff --git a/plugins/services/neo4j.go b/plugins/services/neo4j.go index 5c172c9..9b0c1f0 100644 --- a/plugins/services/neo4j.go +++ b/plugins/services/neo4j.go @@ -27,15 +27,14 @@ func NewNeo4jPlugin() *Neo4jPlugin { func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { config := session.Config - state := session.State target := info.Target() if config.DisableBrute { - return p.identifyService(ctx, info, config, state) + return p.identifyService(ctx, info, session) } // 先测试未授权访问 - if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success { + if result := p.testUnauthorizedAccess(ctx, info, session); result != nil && result.Success { common.LogVuln(i18n.Tr("neo4j_unauth", target)) return result } @@ -50,7 +49,7 @@ func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo, session * } // 使用公共框架进行并发凭据测试 - authFn := p.createAuthFunc(info, config, state) + authFn := p.createAuthFunc(info, session) testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "neo4j", testConfig) @@ -63,14 +62,15 @@ func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo, session * } // createAuthFunc 创建Neo4j认证函数 -func (p *Neo4jPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc { +func (p *Neo4jPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc { return func(ctx context.Context, cred Credential) *AuthResult { - return p.doNeo4jAuth(ctx, info, cred, config, state) + return p.doNeo4jAuth(ctx, info, cred, session) } } // doNeo4jAuth 执行Neo4j认证 -func (p *Neo4jPlugin) doNeo4jAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { +func (p *Neo4jPlugin) doNeo4jAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult { + config := session.Config baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port) client := &http.Client{Timeout: config.Timeout} @@ -87,16 +87,14 @@ func (p *Neo4jPlugin) doNeo4jAuth(ctx context.Context, info *common.HostInfo, cr req.SetBasicAuth(cred.Username, cred.Password) req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) + resp, err := session.HTTPDo(client, req) if err != nil { - state.IncrementTCPFailedPacketCount() return &AuthResult{ Success: false, ErrorType: classifyNeo4jErrorType(err), Error: err, } } - state.IncrementTCPSuccessPacketCount() defer func() { _ = resp.Body.Close() }() if resp.StatusCode == 200 { @@ -147,7 +145,8 @@ func classifyNeo4jErrorType(err error) ErrorType { return ClassifyError(err, neo4jAuthErrors, CommonNetworkErrors) } -func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port) client := &http.Client{Timeout: config.Timeout} @@ -157,12 +156,10 @@ func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H return nil } - resp, err := client.Do(req) + resp, err := session.HTTPDo(client, req) if err != nil { - state.IncrementTCPFailedPacketCount() return nil } - state.IncrementTCPSuccessPacketCount() defer func() { _ = resp.Body.Close() }() if resp.StatusCode == 200 { @@ -177,7 +174,8 @@ func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H return nil } -func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config target := info.Target() baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port) @@ -192,16 +190,14 @@ func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo } } - resp, err := client.Do(req) + resp, err := session.HTTPDo(client, req) if err != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "neo4j", Error: err, } } - state.IncrementTCPSuccessPacketCount() defer func() { _ = resp.Body.Close() }() var banner string diff --git a/plugins/services/rabbitmq.go b/plugins/services/rabbitmq.go index 45bf91a..4a57de1 100644 --- a/plugins/services/rabbitmq.go +++ b/plugins/services/rabbitmq.go @@ -28,7 +28,6 @@ func NewRabbitMQPlugin() *RabbitMQPlugin { func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { config := session.Config - state := session.State target := info.Target() if config.DisableBrute { @@ -36,7 +35,7 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, sessio } // 先检测未授权访问 - if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success { + if result := p.testUnauthorizedAccess(ctx, info, session); result != nil && result.Success { common.LogSuccess(i18n.Tr("rabbitmq_service", target, result.Banner)) return result } @@ -51,7 +50,7 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, sessio } // 使用公共框架进行并发凭据测试 - authFn := p.createAuthFunc(info, config, state) + authFn := p.createAuthFunc(info, session) testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "rabbitmq", testConfig) @@ -64,14 +63,15 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, sessio } // createAuthFunc 创建RabbitMQ认证函数 -func (p *RabbitMQPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc { +func (p *RabbitMQPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc { return func(ctx context.Context, cred Credential) *AuthResult { - return p.doRabbitMQAuth(ctx, info, cred, config, state) + return p.doRabbitMQAuth(ctx, info, cred, session) } } // doRabbitMQAuth 执行RabbitMQ认证 -func (p *RabbitMQPlugin) doRabbitMQAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { +func (p *RabbitMQPlugin) doRabbitMQAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult { + config := session.Config // 对于AMQP端口,使用HTTP管理接口 port := info.Port if port == 5672 || port == 5671 { @@ -96,16 +96,14 @@ func (p *RabbitMQPlugin) doRabbitMQAuth(ctx context.Context, info *common.HostIn req.SetBasicAuth(cred.Username, cred.Password) req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) + resp, err := session.HTTPDo(client, req) if err != nil { - state.IncrementTCPFailedPacketCount() return &AuthResult{ Success: false, ErrorType: classifyRabbitMQErrorType(err), Error: err, } } - state.IncrementTCPSuccessPacketCount() defer func() { _ = resp.Body.Close() }() if resp.StatusCode == 200 { @@ -157,7 +155,8 @@ func classifyRabbitMQErrorType(err error) ErrorType { } // testUnauthorizedAccess 测试RabbitMQ未授权访问 -func (p *RabbitMQPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *RabbitMQPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config port := info.Port if port == 5672 || port == 5671 { port = 15672 @@ -172,11 +171,9 @@ func (p *RabbitMQPlugin) testUnauthorizedAccess(ctx context.Context, info *commo return nil } - resp, err := client.Do(req) + resp, err := session.HTTPDo(client, req) if err != nil { - state.IncrementTCPFailedPacketCount() } else { - state.IncrementTCPSuccessPacketCount() defer func() { _ = resp.Body.Close() }() if resp.StatusCode == 200 { @@ -193,7 +190,7 @@ func (p *RabbitMQPlugin) testUnauthorizedAccess(ctx context.Context, info *commo guestReq, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/api/overview", nil) if err == nil { guestReq.SetBasicAuth("guest", "guest") - guestResp, guestErr := client.Do(guestReq) + guestResp, guestErr := session.HTTPDo(client, guestReq) if guestErr == nil { defer func() { _ = guestResp.Body.Close() }() if guestResp.StatusCode == 200 { @@ -263,7 +260,6 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { config := session.Config - state := session.State target := info.Target() baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port) @@ -278,16 +274,14 @@ func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *comm } } - resp, err := client.Do(req) + resp, err := session.HTTPDo(client, req) if err != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "rabbitmq", Error: err, } } - state.IncrementTCPSuccessPacketCount() defer func() { _ = resp.Body.Close() }() if resp.StatusCode == 200 || resp.StatusCode == 401 { diff --git a/plugins/web/webtitle.go b/plugins/web/webtitle.go index 9c6f69b..70f0210 100644 --- a/plugins/web/webtitle.go +++ b/plugins/web/webtitle.go @@ -82,7 +82,7 @@ func (p *WebTitlePlugin) Scan(ctx context.Context, info *common.HostInfo, sessio func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo, config *common.Config, session *common.ScanSession) (string, int, int, string, []string, string, error) { // 智能协议检测 - protocol := p.detectProtocol(info, config, session) + protocol := p.detectProtocol(ctx, info, config, session) isGM := false urlScheme := protocol if protocol == "https-gm" { @@ -250,7 +250,7 @@ func (p *WebTitlePlugin) formatHeaders(headers http.Header) string { } // detectProtocol 智能检测HTTP/HTTPS协议(基于服务识别和主动探测) -func (p *WebTitlePlugin) detectProtocol(info *common.HostInfo, config *common.Config, session *common.ScanSession) string { +func (p *WebTitlePlugin) detectProtocol(ctx context.Context, info *common.HostInfo, config *common.Config, session *common.ScanSession) string { host := info.Host port := info.Port @@ -277,7 +277,7 @@ func (p *WebTitlePlugin) detectProtocol(info *common.HostInfo, config *common.Co // 第三优先级:主动协议检测(TLS握手) // 对于-u模式或服务名为普通"http"的情况,进行主动检测确认 - detected := core.DetectHTTPScheme(host, port, config, session) + detected := core.DetectHTTPSchemeContext(ctx, host, port, config, session) if detected != "" { // 缓存检测结果(避免重复检测) if exists {