Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
348 lines
10 KiB
Go
348 lines
10 KiB
Go
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 := `<html><head><title>Upload</title></head><body>
|
|
<form action="/import" method="post" enctype="multipart/form-data">
|
|
<input type="file" name="payload">
|
|
<input type="text" name="webhook_url" value="">
|
|
</form>
|
|
<a href="/admin/login">Admin</a>
|
|
<link href="/wp-content/themes/x/style.css">
|
|
</body></html>`
|
|
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(`<html><a href="/page2">next</a><a href="http://evil.example/x">off</a></html>`))
|
|
case "/page2":
|
|
w.Write([]byte(`<html><a href="/page3">deep</a></html>`))
|
|
case "/page3":
|
|
w.Write([]byte(`<html>leaf</html>`))
|
|
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, `<html><form enctype="multipart/form-data"><input type="file" name="f"></form></html>`, nil
|
|
})
|
|
t.Cleanup(func() { SetFetchPageHook(nil) })
|
|
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) })
|
|
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(`<html><body><form action="/api/upload" enctype="multipart/form-data"><input type="file"></form><script src="/static/upload.js"></script></body></html>`))
|
|
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(`<html><form enctype="multipart/form-data" action="/api/upload"><input type="file"></form></html>`))
|
|
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) })
|
|
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) })
|
|
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/", `<input name="avatar_url" id="userAvatar">`)
|
|
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) }
|
|
}
|