mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
fix: harden scan edge cases
This commit is contained in:
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user