fix: harden scan edge cases

This commit is contained in:
ZacharyZcR
2026-06-01 03:32:13 +08:00
parent 8b558b4f12
commit 8ec96bfe6d
21 changed files with 534 additions and 67 deletions
+28 -2
View File
@@ -163,6 +163,21 @@ func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode == 200 {
body, err := io.ReadAll(resp.Body)
if err != nil {
return &ScanResult{
Success: false,
Service: "neo4j",
Error: err,
}
}
if !strings.Contains(strings.ToLower(string(body)), "neo4j") {
return &ScanResult{
Success: false,
Service: "neo4j",
Error: fmt.Errorf("%s", i18n.Tr("service_not_identified", "Neo4j")),
}
}
return &ScanResult{
Type: plugins.ResultTypeVuln,
Success: true,
@@ -206,11 +221,22 @@ func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo
if serverHeader != "" && strings.Contains(strings.ToLower(serverHeader), "neo4j") {
banner = "Neo4j"
} else if resp.StatusCode == 200 || resp.StatusCode == 401 {
body, _ := io.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return &ScanResult{
Success: false,
Service: "neo4j",
Error: err,
}
}
if strings.Contains(strings.ToLower(string(body)), "neo4j") {
banner = "Neo4j"
} else {
banner = "Neo4j"
return &ScanResult{
Success: false,
Service: "neo4j",
Error: fmt.Errorf("%s", i18n.Tr("service_not_identified", "Neo4j")),
}
}
} else {
return &ScanResult{
+59
View File
@@ -0,0 +1,59 @@
package services
import (
"context"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"github.com/shadow1ng/fscan/common"
)
func testSession() *common.ScanSession {
cfg := common.NewConfig()
return common.NewScanSession(cfg, common.NewState(), &common.FlagVars{})
}
func hostInfoFromServer(t *testing.T, server *httptest.Server) *common.HostInfo {
t.Helper()
u, err := url.Parse(server.URL)
if err != nil {
t.Fatalf("Parse server URL error = %v", err)
}
host, portText, err := net.SplitHostPort(u.Host)
if err != nil {
t.Fatalf("SplitHostPort error = %v", err)
}
port, err := strconv.Atoi(portText)
if err != nil {
t.Fatalf("Atoi port error = %v", err)
}
return &common.HostInfo{Host: host, Port: port}
}
func TestNeo4jIdentifyRejectsGenericHTTP(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("plain http service"))
}))
defer server.Close()
result := NewNeo4jPlugin().identifyService(context.Background(), hostInfoFromServer(t, server), testSession())
if result.Success {
t.Fatalf("identifyService reported generic HTTP as Neo4j: %#v", result)
}
}
func TestNeo4jUnauthorizedRequiresNeo4jBody(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
}))
defer server.Close()
result := NewNeo4jPlugin().testUnauthorizedAccess(context.Background(), hostInfoFromServer(t, server), testSession())
if result != nil && result.Success {
t.Fatalf("testUnauthorizedAccess reported generic 200 as Neo4j: %#v", result)
}
}
+8 -1
View File
@@ -285,7 +285,14 @@ func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *comm
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode == 200 || resp.StatusCode == 401 {
body, _ := io.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return &ScanResult{
Success: false,
Service: "rabbitmq",
Error: err,
}
}
if strings.Contains(strings.ToLower(string(body)), "rabbitmq") {
banner := "RabbitMQ Management"
common.LogSuccess(i18n.Tr("rabbitmq_detected", target, banner))
+20
View File
@@ -0,0 +1,20 @@
package services
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
func TestRabbitMQManagementRejectsGenericHTTP(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("plain http service"))
}))
defer server.Close()
result := NewRabbitMQPlugin().testManagementInterface(context.Background(), hostInfoFromServer(t, server), testSession())
if result.Success {
t.Fatalf("testManagementInterface reported generic HTTP as RabbitMQ: %#v", result)
}
}
+7 -8
View File
@@ -124,7 +124,7 @@ func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo,
body, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
contentLen := len(body)
if contentLen <= 0 && err != nil {
if err != nil {
return "", resp.StatusCode, 0, resp.Header.Get("Server"), nil, displayURL, err
}
@@ -133,7 +133,7 @@ func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo,
checkDataList = append(checkDataList, WebScan.CheckDatas{
Body: body,
Headers: p.formatHeaders(resp.Header),
Favicon: p.fetchFaviconHash(baseURL),
Favicon: p.fetchFaviconHash(ctx, baseURL),
})
title := p.extractTitle(string(body))
@@ -153,15 +153,14 @@ func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo,
reqRedirect.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
respRedirect, err := clientR.Do(reqRedirect)
if err == nil {
bodyRedirect, _ := io.ReadAll(respRedirect.Body)
bodyRedirect, err := io.ReadAll(respRedirect.Body)
_ = respRedirect.Body.Close()
if len(bodyRedirect) > 0 {
if err == nil && len(bodyRedirect) > 0 {
// 添加跳转后页面的指纹数据
checkDataList = append(checkDataList, WebScan.CheckDatas{
Body: bodyRedirect,
Headers: p.formatHeaders(respRedirect.Header),
Favicon: p.fetchFaviconHash(redirectURL),
Favicon: p.fetchFaviconHash(ctx, redirectURL),
})
// 如果原始页面没有标题,使用跳转后页面的标题
@@ -314,7 +313,7 @@ func (p *WebTitlePlugin) extractTitle(html string) string {
}
// fetchFaviconHash 下载 favicon.ico 并计算 hash
func (p *WebTitlePlugin) fetchFaviconHash(baseURL string) fingerprint.FaviconHashes {
func (p *WebTitlePlugin) fetchFaviconHash(ctx context.Context, baseURL string) fingerprint.FaviconHashes {
// 构造 favicon URL
u, err := url.Parse(baseURL)
if err != nil {
@@ -323,7 +322,7 @@ func (p *WebTitlePlugin) fetchFaviconHash(baseURL string) fingerprint.FaviconHas
faviconURL := fmt.Sprintf("%s://%s/favicon.ico", u.Scheme, u.Host)
// 请求 favicon
req, err := http.NewRequest("GET", faviconURL, nil)
req, err := http.NewRequestWithContext(ctx, "GET", faviconURL, nil)
if err != nil {
return fingerprint.FaviconHashes{}
}
+37
View File
@@ -0,0 +1,37 @@
package web
import (
"context"
"net/http"
"testing"
"github.com/shadow1ng/fscan/webscan/lib"
)
type faviconRoundTripper struct {
called bool
}
func (rt *faviconRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
rt.called = true
<-req.Context().Done()
return nil, req.Context().Err()
}
func TestFetchFaviconHashHonorsContext(t *testing.T) {
previous := lib.Client
rt := &faviconRoundTripper{}
lib.Client = &http.Client{Transport: rt}
defer func() { lib.Client = previous }()
ctx, cancel := context.WithCancel(context.Background())
cancel()
hashes := NewWebTitlePlugin().fetchFaviconHash(ctx, "http://example.com")
if !rt.called {
t.Fatal("favicon client was not called")
}
if len(hashes.MMH3) != 0 || len(hashes.MD5) != 0 {
t.Fatalf("fetchFaviconHash returned hashes for canceled context: %#v", hashes)
}
}