From 4198c1abc83ece3bd877df077f82f217e3d806a0 Mon Sep 17 00:00:00 2001 From: ZacharyZcR <2903735704@qq.com> Date: Thu, 4 Jun 2026 14:41:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=BB=98=E8=AE=A4=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=20POC=20=E7=BB=93=E6=9E=9C=E7=BC=BA=E5=A4=B1=20#586?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/base_scan_strategy.go | 39 +++++------ core/base_scan_strategy_test.go | 11 ++++ core/port_scan.go | 2 +- webscan/lib/poc_executor.go | 5 +- webscan/lib/poc_executor_test.go | 108 +++++++++++++++++++++++++------ 5 files changed, 125 insertions(+), 40 deletions(-) diff --git a/core/base_scan_strategy.go b/core/base_scan_strategy.go index f78b813..9bf9abc 100644 --- a/core/base_scan_strategy.go +++ b/core/base_scan_strategy.go @@ -140,6 +140,9 @@ func (b *BaseScanStrategy) isPluginApplicableToPortWithHost(pluginName string, t } func (b *BaseScanStrategy) isPluginApplicableToPort(pluginName string, targetPort int) bool { + if b.isWebPlugin(pluginName) { + return true + } return b.isPluginApplicableToPortWithHost(pluginName, "", targetPort) } @@ -253,32 +256,32 @@ func (b *BaseScanStrategy) getPluginsByFilterType() []string { filteredPlugins = append(filteredPlugins, pluginName) } } - // 确保 webtitle 在 webpoc 之前执行,避免指纹识别竞态 - sort.Slice(filteredPlugins, func(i, j int) bool { - // webtitle 必须在 webpoc 之前 - if filteredPlugins[i] == "webtitle" { - return true - } - if filteredPlugins[j] == "webtitle" { - return false - } - if filteredPlugins[i] == "webpoc" { - return false - } - if filteredPlugins[j] == "webpoc" { - return true - } - // 其他插件保持字母顺序 - return filteredPlugins[i] < filteredPlugins[j] - }) default: // 无过滤器:返回所有插件 filteredPlugins = allPlugins } + orderWebPlugins(filteredPlugins) return filteredPlugins } +func orderWebPlugins(pluginNames []string) { + sort.SliceStable(pluginNames, func(i, j int) bool { + return webPluginOrder(pluginNames[i]) < webPluginOrder(pluginNames[j]) + }) +} + +func webPluginOrder(pluginName string) int { + switch pluginName { + case "webtitle": + return 0 + case "webpoc": + return 2 + default: + return 1 + } +} + // parsePluginList 解析插件列表字符串 func parsePluginList(pluginStr string) []string { if pluginStr == "" { diff --git a/core/base_scan_strategy_test.go b/core/base_scan_strategy_test.go index 1db4ede..b54d110 100644 --- a/core/base_scan_strategy_test.go +++ b/core/base_scan_strategy_test.go @@ -261,6 +261,17 @@ func slicesEqual(a, b []string) bool { return true } +func TestOrderWebPlugins(t *testing.T) { + plugins := []string{"ssh", "webpoc", "redis", "webtitle", "mysql"} + + orderWebPlugins(plugins) + + expected := []string{"webtitle", "ssh", "redis", "mysql", "webpoc"} + if !slicesEqual(plugins, expected) { + t.Fatalf("orderWebPlugins = %#v, want %#v", plugins, expected) + } +} + // TestNewBaseScanStrategy 测试构造函数 func TestNewBaseScanStrategy(t *testing.T) { tests := []struct { diff --git a/core/port_scan.go b/core/port_scan.go index d015b55..0d37153 100644 --- a/core/port_scan.go +++ b/core/port_scan.go @@ -516,7 +516,6 @@ func scanSinglePort(ctx context.Context, host string, port int, addr string, ada // 步骤2:记录开放端口 count.Add(1) - collector.Add(addr) saveOpenPort(session, host, port) // 步骤3:服务识别(Scanner负责关闭连接,包括探测中可能创建的新连接) @@ -535,6 +534,7 @@ func scanSinglePort(ctx context.Context, host string, port int, addr string, ada // 步骤4:处理结果 processServiceResult(ctx, host, port, addr, serviceInfo, config, session) + collector.Add(addr) } // handleConnectionFailure 处理连接失败 diff --git a/webscan/lib/poc_executor.go b/webscan/lib/poc_executor.go index de95b35..d55aae6 100644 --- a/webscan/lib/poc_executor.go +++ b/webscan/lib/poc_executor.go @@ -326,7 +326,10 @@ func executeRules(oReq *http.Request, p *Poc, variableMap map[string]interface{} success := false if len(p.Rules) > 0 { success = executeRuleSet(p.Rules) - return success, "", nil + if success { + return true, p.Name, nil + } + return false, "", nil } for _, item := range p.Groups { name, rules := item.Key, item.Value diff --git a/webscan/lib/poc_executor_test.go b/webscan/lib/poc_executor_test.go index 8e1f15f..3677e7f 100644 --- a/webscan/lib/poc_executor_test.go +++ b/webscan/lib/poc_executor_test.go @@ -1,8 +1,15 @@ package lib import ( + "context" + "net/http" + "net/http/httptest" "strings" "testing" + "time" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/output" ) // ============================================================================= @@ -102,42 +109,103 @@ func TestGetRuleHash(t *testing.T) { } } +func TestCheckMultiPocSavesSimpleRulesPoc(t *testing.T) { + paths := make(chan string, 1) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case paths <- r.URL.Path: + default: + } + _, _ = w.Write([]byte("kei-poc-hit")) + })) + defer server.Close() + + cfg := common.NewConfig() + cfg.Output.Silent = true + cfg.Network.WebTimeout = 5 * time.Second + cfg.Network.MaxRedirects = 3 + cfg.POC.Num = 1 + if err := Inithttp(cfg); err != nil { + t.Fatalf("Inithttp: %v", err) + } + + var results []*output.ScanResult + session := common.NewScanSession(cfg, common.NewState(), &common.FlagVars{}) + session.ResultSink = func(result *output.ScanResult) error { + results = append(results, result) + return nil + } + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil) + if err != nil { + t.Fatalf("NewRequest: %v", err) + } + + poc := &Poc{ + Name: "poc-yaml-kei-repro", + Rules: []Rules{{ + Method: http.MethodGet, + Path: "/kei-poc", + Expression: `response.status == 200 && response.body.bcontains(b"kei-poc-hit")`, + }}, + } + CheckMultiPoc(req, []*Poc{poc}, 1, &POCContext{Session: session}) + + select { + case got := <-paths: + if got != "/kei-poc" { + t.Fatalf("request path = %q, want /kei-poc", got) + } + default: + t.Fatal("POC request was not sent") + } + if len(results) != 1 { + t.Fatalf("saved results = %d, want 1", len(results)) + } + if results[0].Type != output.TypeVuln || results[0].Target != server.URL { + t.Fatalf("saved result = %#v", results[0]) + } + if got := results[0].Details["vulnerability_name"]; got != "poc-yaml-kei-repro" { + t.Fatalf("vulnerability_name = %v, want poc-yaml-kei-repro", got) + } +} + // TestDoSearchSetCookieOptimization 测试 Set-Cookie 提取和清理 func TestDoSearchSetCookieOptimization(t *testing.T) { responseHeaders := "HTTP/1.1 200 OK\r\n" cases := []struct { - name string - regex string - body string - wantContain string // 期望结果包含的内容 + name string + regex string + body string + wantContain string // 期望结果包含的内容 wantNotContain string // 期望结果不包含的内容 }{ { - name: "捕获组名为cookie时清理属性", - regex: `Set-Cookie:(?P.*)`, - body: responseHeaders + "Set-Cookie: sessionid=abc123; Path=/; HttpOnly\r\n\r\n", - wantContain: "sessionid=abc123", + name: "捕获组名为cookie时清理属性", + regex: `Set-Cookie:(?P.*)`, + body: responseHeaders + "Set-Cookie: sessionid=abc123; Path=/; HttpOnly\r\n\r\n", + wantContain: "sessionid=abc123", wantNotContain: "Path", }, { - name: "捕获组名为sessid时也清理属性", - regex: `Set-Cookie:(?P.*)`, - body: responseHeaders + "Set-Cookie: JSESSIONID=xyz789; Path=/app; Secure; HttpOnly\r\n\r\n{}", - wantContain: "JSESSIONID=xyz789", + name: "捕获组名为sessid时也清理属性", + regex: `Set-Cookie:(?P.*)`, + body: responseHeaders + "Set-Cookie: JSESSIONID=xyz789; Path=/app; Secure; HttpOnly\r\n\r\n{}", + wantContain: "JSESSIONID=xyz789", wantNotContain: "Secure", }, { - name: "捕获组名为token时也清理属性", - regex: `Set-Cookie:(?P.*)`, - body: responseHeaders + "Set-Cookie: csrf_token=tok123; Max-Age=3600; SameSite=Strict\r\n\r\nOK", - wantContain: "csrf_token=tok123", + name: "捕获组名为token时也清理属性", + regex: `Set-Cookie:(?P.*)`, + body: responseHeaders + "Set-Cookie: csrf_token=tok123; Max-Age=3600; SameSite=Strict\r\n\r\nOK", + wantContain: "csrf_token=tok123", wantNotContain: "Max-Age", }, { - name: "非Set-Cookie的正则不触发清理", - regex: `X-Custom:(?P.*)`, - body: responseHeaders + "X-Custom: some-value; extra=stuff\r\n\r\ndone", - wantContain: "some-value; extra=stuff", + name: "非Set-Cookie的正则不触发清理", + regex: `X-Custom:(?P.*)`, + body: responseHeaders + "X-Custom: some-value; extra=stuff\r\n\r\ndone", + wantContain: "some-value; extra=stuff", }, }