Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
POST /api/v1/recon/scan probes fleet ports from the server host, crawls owned HTTP targets, maps findings to spread lanes, and records optional oath ledger rows.
187 lines
4.4 KiB
Go
187 lines
4.4 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")
|
|
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)
|
|
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) })
|
|
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 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
|
|
}
|