mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
修复默认扫描 POC 结果缺失 #586
This commit is contained in:
+21
-18
@@ -140,6 +140,9 @@ func (b *BaseScanStrategy) isPluginApplicableToPortWithHost(pluginName string, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseScanStrategy) isPluginApplicableToPort(pluginName string, targetPort int) bool {
|
func (b *BaseScanStrategy) isPluginApplicableToPort(pluginName string, targetPort int) bool {
|
||||||
|
if b.isWebPlugin(pluginName) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return b.isPluginApplicableToPortWithHost(pluginName, "", targetPort)
|
return b.isPluginApplicableToPortWithHost(pluginName, "", targetPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,32 +256,32 @@ func (b *BaseScanStrategy) getPluginsByFilterType() []string {
|
|||||||
filteredPlugins = append(filteredPlugins, pluginName)
|
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:
|
default:
|
||||||
// 无过滤器:返回所有插件
|
// 无过滤器:返回所有插件
|
||||||
filteredPlugins = allPlugins
|
filteredPlugins = allPlugins
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orderWebPlugins(filteredPlugins)
|
||||||
return 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 解析插件列表字符串
|
// parsePluginList 解析插件列表字符串
|
||||||
func parsePluginList(pluginStr string) []string {
|
func parsePluginList(pluginStr string) []string {
|
||||||
if pluginStr == "" {
|
if pluginStr == "" {
|
||||||
|
|||||||
@@ -261,6 +261,17 @@ func slicesEqual(a, b []string) bool {
|
|||||||
return true
|
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 测试构造函数
|
// TestNewBaseScanStrategy 测试构造函数
|
||||||
func TestNewBaseScanStrategy(t *testing.T) {
|
func TestNewBaseScanStrategy(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|||||||
+1
-1
@@ -516,7 +516,6 @@ func scanSinglePort(ctx context.Context, host string, port int, addr string, ada
|
|||||||
|
|
||||||
// 步骤2:记录开放端口
|
// 步骤2:记录开放端口
|
||||||
count.Add(1)
|
count.Add(1)
|
||||||
collector.Add(addr)
|
|
||||||
saveOpenPort(session, host, port)
|
saveOpenPort(session, host, port)
|
||||||
|
|
||||||
// 步骤3:服务识别(Scanner负责关闭连接,包括探测中可能创建的新连接)
|
// 步骤3:服务识别(Scanner负责关闭连接,包括探测中可能创建的新连接)
|
||||||
@@ -535,6 +534,7 @@ func scanSinglePort(ctx context.Context, host string, port int, addr string, ada
|
|||||||
|
|
||||||
// 步骤4:处理结果
|
// 步骤4:处理结果
|
||||||
processServiceResult(ctx, host, port, addr, serviceInfo, config, session)
|
processServiceResult(ctx, host, port, addr, serviceInfo, config, session)
|
||||||
|
collector.Add(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleConnectionFailure 处理连接失败
|
// handleConnectionFailure 处理连接失败
|
||||||
|
|||||||
@@ -326,7 +326,10 @@ func executeRules(oReq *http.Request, p *Poc, variableMap map[string]interface{}
|
|||||||
success := false
|
success := false
|
||||||
if len(p.Rules) > 0 {
|
if len(p.Rules) > 0 {
|
||||||
success = executeRuleSet(p.Rules)
|
success = executeRuleSet(p.Rules)
|
||||||
return success, "", nil
|
if success {
|
||||||
|
return true, p.Name, nil
|
||||||
|
}
|
||||||
|
return false, "", nil
|
||||||
}
|
}
|
||||||
for _, item := range p.Groups {
|
for _, item := range p.Groups {
|
||||||
name, rules := item.Key, item.Value
|
name, rules := item.Key, item.Value
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package lib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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 提取和清理
|
// TestDoSearchSetCookieOptimization 测试 Set-Cookie 提取和清理
|
||||||
func TestDoSearchSetCookieOptimization(t *testing.T) {
|
func TestDoSearchSetCookieOptimization(t *testing.T) {
|
||||||
responseHeaders := "HTTP/1.1 200 OK\r\n"
|
responseHeaders := "HTTP/1.1 200 OK\r\n"
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
regex string
|
regex string
|
||||||
body string
|
body string
|
||||||
wantContain string // 期望结果包含的内容
|
wantContain string // 期望结果包含的内容
|
||||||
wantNotContain string // 期望结果不包含的内容
|
wantNotContain string // 期望结果不包含的内容
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "捕获组名为cookie时清理属性",
|
name: "捕获组名为cookie时清理属性",
|
||||||
regex: `Set-Cookie:(?P<cookie>.*)`,
|
regex: `Set-Cookie:(?P<cookie>.*)`,
|
||||||
body: responseHeaders + "Set-Cookie: sessionid=abc123; Path=/; HttpOnly\r\n\r\n<html></html>",
|
body: responseHeaders + "Set-Cookie: sessionid=abc123; Path=/; HttpOnly\r\n\r\n<html></html>",
|
||||||
wantContain: "sessionid=abc123",
|
wantContain: "sessionid=abc123",
|
||||||
wantNotContain: "Path",
|
wantNotContain: "Path",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "捕获组名为sessid时也清理属性",
|
name: "捕获组名为sessid时也清理属性",
|
||||||
regex: `Set-Cookie:(?P<sessid>.*)`,
|
regex: `Set-Cookie:(?P<sessid>.*)`,
|
||||||
body: responseHeaders + "Set-Cookie: JSESSIONID=xyz789; Path=/app; Secure; HttpOnly\r\n\r\n{}",
|
body: responseHeaders + "Set-Cookie: JSESSIONID=xyz789; Path=/app; Secure; HttpOnly\r\n\r\n{}",
|
||||||
wantContain: "JSESSIONID=xyz789",
|
wantContain: "JSESSIONID=xyz789",
|
||||||
wantNotContain: "Secure",
|
wantNotContain: "Secure",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "捕获组名为token时也清理属性",
|
name: "捕获组名为token时也清理属性",
|
||||||
regex: `Set-Cookie:(?P<token>.*)`,
|
regex: `Set-Cookie:(?P<token>.*)`,
|
||||||
body: responseHeaders + "Set-Cookie: csrf_token=tok123; Max-Age=3600; SameSite=Strict\r\n\r\nOK",
|
body: responseHeaders + "Set-Cookie: csrf_token=tok123; Max-Age=3600; SameSite=Strict\r\n\r\nOK",
|
||||||
wantContain: "csrf_token=tok123",
|
wantContain: "csrf_token=tok123",
|
||||||
wantNotContain: "Max-Age",
|
wantNotContain: "Max-Age",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "非Set-Cookie的正则不触发清理",
|
name: "非Set-Cookie的正则不触发清理",
|
||||||
regex: `X-Custom:(?P<value>.*)`,
|
regex: `X-Custom:(?P<value>.*)`,
|
||||||
body: responseHeaders + "X-Custom: some-value; extra=stuff\r\n\r\ndone",
|
body: responseHeaders + "X-Custom: some-value; extra=stuff\r\n\r\ndone",
|
||||||
wantContain: "some-value; extra=stuff",
|
wantContain: "some-value; extra=stuff",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user