package recon import ( "encoding/json" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" ) func TestScanPortsWithInject(t *testing.T) { SetPortDialHook(func(host string, port int, _ time.Duration) bool { if host != "10.0.0.5" { t.Fatalf("host=%q", host) } return port == 22 || port == 443 }) t.Cleanup(func() { SetPortDialHook(nil) }) results := ScanPorts("10.0.0.5", nil) open := map[int]bool{} for _, r := range results { if r.Open { open[r.Port] = true } } if !open[22] || !open[443] { t.Fatalf("open=%v", open) } if open[445] { t.Fatal("445 should be closed") } } func TestParseHTMLFixtures(t *testing.T) { body := `Upload
Admin ` files, multi, fields, score, cms := ParseHTML("http://lab/upload", body) if len(files) != 1 || !files[0].HasFile { t.Fatalf("file inputs: %+v", files) } if len(multi) != 1 || !multi[0].Multipart { t.Fatalf("multipart: %+v", multi) } if len(fields) == 0 { t.Fatal("expected url fields") } if score < 10 { t.Fatalf("ssrf score=%d", score) } if !containsStr(cms, "wordpress") && !containsStr(cms, "admin_login") { t.Fatalf("cms=%v", cms) } } func TestCrawlSameOriginDepth(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": w.Write([]byte(`nextoff`)) case "/page2": w.Write([]byte(`deep`)) case "/page3": w.Write([]byte(`leaf`)) default: http.NotFound(w, r) } })) defer srv.Close() u, err := url.Parse(srv.URL) if err != nil { t.Fatal(err) } port := 80 if p := u.Port(); p != "" { port = atoi(p) } SetFetchPageHook(func(rawURL string) (int, string, error) { resp, err := http.Get(rawURL) if err != nil { return 0, "", err } defer resp.Body.Close() body, _ := readBodyLimited(resp.Body, maxHTMLBytes) return resp.StatusCode, body, nil }) t.Cleanup(func() { SetFetchPageHook(nil) }) report, err := Crawl(u.Hostname(), port, u.Scheme, []string{"/"}) if err != nil { t.Fatal(err) } if report.PagesFetched < 2 { t.Fatalf("pages=%d", report.PagesFetched) } for _, p := range report.Pages { if strings.Contains(p.URL, "evil.example") { t.Fatalf("followed off-origin %s", p.URL) } } } func TestScanReportRecommendations(t *testing.T) { ports := []PortResult{ {Port: 22, Open: true}, {Port: 5985, Open: true}, {Port: 80, Open: true}, } crawl := &CrawlReport{ MultipartForms: []FormFinding{{PageURL: "http://x/", Multipart: true}}, SSRFScore: 40, } recs := BuildRecommendations(ports, crawl, nil, "10.0.0.1", false) if len(recs) < 5 { t.Fatalf("recs=%+v", recs) } keys := map[string]bool{} for _, r := range recs { keys[r.Lane+"|"+r.Template] = true } for _, want := range []string{"linux_lotl|linux-lotl", "winrm|winrm", "bits_curl|", "stage_fetch|", "|ssrf_probe", "|public_waterhole"} { if !keys[want] { t.Fatalf("missing %q in %+v", want, recs) } } } func TestScanOwnedTarget(t *testing.T) { SetPortDialHook(func(host string, port int, _ time.Duration) bool { return port == 80 }) t.Cleanup(func() { SetPortDialHook(nil) }) SetFetchPageHook(func(rawURL string) (int, string, error) { return 200, `
`, nil }) t.Cleanup(func() { SetFetchPageHook(nil) }) report, err := Scan(ScanRequest{Host: "owned.lab", Port: 80, Scheme: "http"}) if err != nil { t.Fatal(err) } if report.Host != "owned.lab" { t.Fatalf("host=%q", report.Host) } if report.Crawl == nil || len(report.Recommendations) == 0 { raw, _ := json.Marshal(report) t.Fatalf("report=%s", raw) } } func TestNormalizeOwnedHostRejectsEmpty(t *testing.T) { if _, err := Scan(ScanRequest{}); err == nil { t.Fatal("expected error") } } func TestUploadHunterMultipartAndJS(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": w.Write([]byte(`
`)) case "/static/upload.js": w.Write([]byte(`fetch("/api/upload")`)) case "/api/upload": w.WriteHeader(http.StatusOK) default: http.NotFound(w, r) } })) defer srv.Close() u, _ := url.Parse(srv.URL) port := 80 if p := u.Port(); p != "" { port = atoi(p) } SetFetchPageHook(func(rawURL string) (int, string, error) { resp, err := http.Get(rawURL) if err != nil { return 0, "", err } defer resp.Body.Close() body, _ := readBodyLimited(resp.Body, maxHTMLBytes) return resp.StatusCode, body, nil }) t.Cleanup(func() { SetFetchPageHook(nil) }) report, err := Crawl(u.Hostname(), port, u.Scheme, []string{"/"}) if err != nil || len(report.UploadHunter) == 0 { t.Fatalf("err=%v hunter=%+v", err, report.UploadHunter) } } func TestProbeAdminSurfaceSignals(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/admin", "/admin/": w.WriteHeader(http.StatusForbidden) case "/swagger", "/swagger/": w.WriteHeader(http.StatusOK) default: http.NotFound(w, r) } })) defer srv.Close() u, _ := url.Parse(srv.URL) port := 80 if p := u.Port(); p != "" { port = atoi(p) } SetFetchPageHook(func(rawURL string) (int, string, error) { resp, err := http.Get(rawURL) if err != nil { return 0, "", err } defer resp.Body.Close() body, _ := readBodyLimited(resp.Body, maxHTMLBytes) return resp.StatusCode, body, nil }) t.Cleanup(func() { SetFetchPageHook(nil) }) if len(ProbeAdminSurface(u.Hostname(), port, u.Scheme)) == 0 { t.Fatal("expected admin surface") } } func TestScanReportIncludesAdminSurfaceJSON(t *testing.T) { SetPortDialHook(func(host string, port int, _ time.Duration) bool { return port == 80 }) t.Cleanup(func() { SetPortDialHook(nil) }) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": w.Write([]byte(`
`)) case "/api/upload": w.WriteHeader(http.StatusOK) case "/admin", "/admin/": w.WriteHeader(http.StatusForbidden) default: http.NotFound(w, r) } })) defer srv.Close() u, _ := url.Parse(srv.URL) port := 80 if p := u.Port(); p != "" { port = atoi(p) } SetFetchPageHook(func(rawURL string) (int, string, error) { resp, err := http.Get(rawURL) if err != nil { return 0, "", err } defer resp.Body.Close() body, _ := readBodyLimited(resp.Body, maxHTMLBytes) return resp.StatusCode, body, nil }) t.Cleanup(func() { SetFetchPageHook(nil) }) report, err := Scan(ScanRequest{Host: u.Hostname(), Port: port, Scheme: u.Scheme}) if err != nil { t.Fatal(err) } raw, _ := json.Marshal(report) s := string(raw) if !strings.Contains(s, `"admin_surface"`) || !strings.Contains(s, `"upload_hunter"`) { t.Fatalf("json=%s", s) } } func containsStr(list []string, want string) bool { for _, s := range list { if s == want { return true } } return false } func atoi(s string) int { n := 0 for _, c := range s { if c < '0' || c > '9' { return 80 } n = n*10 + int(c-'0') } if n == 0 { return 80 } return n } func TestResolveScanPortsMergesProfiles(t *testing.T) { ports, used := ResolveScanPorts([]string{"web", "linux", "cloud_metadata"}) if !containsInt(ports, 6262) || len(used) != 3 { t.Fatalf("%v %v", ports, used) } } func TestBuildStackFromHeaders(t *testing.T) { stack := BuildStack([]HTTPHeaderSnap{{Headers: map[string]string{"X-Powered-By":"PHP/8.1"}}}, nil) if SuggestDeployKitLane(stack) != "php" { t.Fatal(stack) } } func TestGrabBannersWithHooks(t *testing.T) { SetBannerHooks(func(_ string,p int) string { if p==22 {return "SSH"}; return "" }, func(_ string,p int)(string,string){ if p==80 {return "t","s"}; return "","" }, func(_ string,p int) string { if p==5985 {return "w"}; return "" }, nil) t.Cleanup(func(){SetBannerHooks(nil,nil,nil,nil)}) if len(GrabBanners("h", []PortResult{{22,true},{80,true},{5985,true}})) != 3 { t.Fatal() } } func TestCloudMetadataProfileSuggestsSSM(t *testing.T) { SetBannerHooks(nil,nil,nil,func()bool{return true}); t.Cleanup(func(){SetBannerHooks(nil,nil,nil,nil)}) for _,r := range BuildRecommendations(nil,nil,nil,"ec2.compute.amazonaws.com",true) { if r.Lane=="ssm_document" { return } } t.Fatal() } func containsInt(a []int,w int) bool { for _,n:=range a { if n==w {return true} }; return false } func TestScoreFormFieldFingerprintKeywords(t *testing.T) { score, matches := ScoreFormFieldFingerprint("callback_url", "", "") if score < 14 || !containsStr(matches, "callback") { t.Fatalf("score=%d matches=%v", score, matches) } } func TestExtractFormFieldFingerprintsExactNameID(t *testing.T) { fps := ExtractFormFieldFingerprints("http://lab/", ``) if len(fps) == 0 || fps[0].Name != "avatar_url" || fps[0].ID != "userAvatar" { t.Fatalf("%+v", fps) } } func TestBuildSSRfCanaryURLForcesHTTPS(t *testing.T) { u := BuildSSRfCanaryURL("http://public.example.com", "abc-123") if !strings.HasPrefix(u, "https://") || !strings.Contains(u, "/recon/ping/abc-123") { t.Fatalf("url=%q", u) } }