Add recon form fingerprint library and SSRF canary mode.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Score crawl inputs by SSRF-prone field names and register ping-back canaries for operator paste confirmation.
This commit is contained in:
@@ -2,66 +2,124 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/recon"
|
||||
)
|
||||
|
||||
func waitForReconScan(t *testing.T, database *db.Database, scanID string) *recon.ScanReport {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
report, err := database.GetReconScan(scanID)
|
||||
if err == nil && report != nil {
|
||||
return report
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("scan %s not persisted", scanID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func withScanIDParam(r *http.Request, scanID string) *http.Request {
|
||||
ctx := chi.NewRouteContext()
|
||||
ctx.URLParams.Add("scan_id", scanID)
|
||||
return r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
|
||||
}
|
||||
|
||||
func TestReconScanEndpoint(t *testing.T) {
|
||||
recon.SetPortDialHook(func(host string, port int, _ time.Duration) bool {
|
||||
return port == 80
|
||||
})
|
||||
recon.SetPortDialHook(func(host string, port int, _ time.Duration) bool { return port == 80 })
|
||||
t.Cleanup(func() { recon.SetPortDialHook(nil) })
|
||||
recon.SetFetchPageHook(func(rawURL string) (int, string, error) {
|
||||
return 200, `<html><form enctype="multipart/form-data"><input type="text" name="preview_url"><input type="file" name="payload"></form></html>`, nil
|
||||
return 200, `<html><form enctype="multipart/form-data"><input type="file" name="payload"></form></html>`, nil
|
||||
})
|
||||
t.Cleanup(func() { recon.SetFetchPageHook(nil) })
|
||||
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
database, _ := db.New(t.TempDir())
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
h := NewReconHandler(database, NewWSHub(database))
|
||||
body, _ := json.Marshal(map[string]interface{}{"host": "recon.lab", "port": 80, "scheme": "http"})
|
||||
w := httptest.NewRecorder()
|
||||
h.Scan(w, httptest.NewRequest(http.MethodPost, "/api/v1/recon/scan", bytes.NewReader(body)))
|
||||
if w.Code != http.StatusAccepted {
|
||||
t.Fatalf("%d %s", w.Code, w.Body.String())
|
||||
}
|
||||
var started map[string]string
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &started)
|
||||
report := waitForReconScan(t, database, started["scan_id"])
|
||||
if report.Host != "recon.lab" || report.Crawl == nil {
|
||||
t.Fatalf("%+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
h := NewReconHandler(database, nil)
|
||||
body, _ := json.Marshal(map[string]interface{}{
|
||||
"host": "recon.lab",
|
||||
"port": 80,
|
||||
"scheme": "http",
|
||||
func TestReconSSRfCanaryFlow(t *testing.T) {
|
||||
recon.SetPortDialHook(func(host string, port int, _ time.Duration) bool { return port == 80 })
|
||||
t.Cleanup(func() { recon.SetPortDialHook(nil) })
|
||||
recon.SetFetchPageHook(func(rawURL string) (int, string, error) {
|
||||
return 200, `<html><input name="callback_url" id="cb">`, nil
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/recon/scan", bytes.NewReader(body))
|
||||
t.Cleanup(func() { recon.SetFetchPageHook(nil) })
|
||||
database, _ := db.New(t.TempDir())
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
h := NewReconHandler(database, nil, func() string { return "https://canary.test" })
|
||||
body, _ := json.Marshal(map[string]interface{}{"host": "recon.lab", "port": 80, "scheme": "http", "ssrf_canary": true})
|
||||
w := httptest.NewRecorder()
|
||||
h.Scan(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
||||
h.Scan(w, httptest.NewRequest(http.MethodPost, "/api/v1/recon/scan", bytes.NewReader(body)))
|
||||
if w.Code != http.StatusAccepted {
|
||||
t.Fatalf("%d", w.Code)
|
||||
}
|
||||
var report recon.ScanReport
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &report); err != nil {
|
||||
t.Fatal(err)
|
||||
var started map[string]string
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &started)
|
||||
report := waitForReconScan(t, database, started["scan_id"])
|
||||
if report.Canary == nil || report.Canary.PasteFieldName != "callback_url" {
|
||||
t.Fatalf("%+v", report.Canary)
|
||||
}
|
||||
if report.Host != "recon.lab" || report.Crawl == nil || len(report.Recommendations) == 0 {
|
||||
t.Fatalf("report=%+v", report)
|
||||
pw := httptest.NewRecorder()
|
||||
h.CanaryPing(pw, withScanIDParam(httptest.NewRequest(http.MethodGet, "/recon/ping/"+report.ScanID, nil), report.ScanID))
|
||||
sw := httptest.NewRecorder()
|
||||
h.CanaryStatus(sw, withScanIDParam(httptest.NewRequest(http.MethodGet, "/api/v1/recon/canary/"+report.ScanID, nil), report.ScanID))
|
||||
var st recon.SSRFCanaryInfo
|
||||
_ = json.Unmarshal(sw.Body.Bytes(), &st)
|
||||
if st.Status != "confirmed" {
|
||||
t.Fatalf("%+v", st)
|
||||
}
|
||||
rows, err := database.ListOathLedger(5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rows) == 0 || rows[0].ActionType != db.OathReconScan {
|
||||
t.Fatalf("oath rows=%+v", rows)
|
||||
if !strings.Contains(report.Canary.PasteTarget, report.ScanID) {
|
||||
t.Fatal(report.Canary.PasteTarget)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconScanRequiresHost(t *testing.T) {
|
||||
h := NewReconHandler(nil, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/recon/scan", bytes.NewReader([]byte(`{}`)))
|
||||
func TestReconHistoryAndExport(t *testing.T) {
|
||||
recon.SetPortDialHook(func(host string, port int, _ time.Duration) bool { return port == 80 })
|
||||
t.Cleanup(func() { recon.SetPortDialHook(nil) })
|
||||
recon.SetFetchPageHook(func(rawURL string) (int, string, error) {
|
||||
return 200, `<html><form action="/up" enctype="multipart/form-data"><input type="file" name="f"></form></html>`, nil
|
||||
})
|
||||
t.Cleanup(func() { recon.SetFetchPageHook(nil) })
|
||||
database, _ := db.New(t.TempDir())
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
h := NewReconHandler(database, nil)
|
||||
body, _ := json.Marshal(map[string]interface{}{"host": "hist.lab", "port": 80, "scheme": "http", "profile": "quick"})
|
||||
w := httptest.NewRecorder()
|
||||
h.Scan(w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status=%d", w.Code)
|
||||
h.Scan(w, httptest.NewRequest(http.MethodPost, "/api/v1/recon/scan", bytes.NewReader(body)))
|
||||
var started map[string]string
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &started)
|
||||
waitForReconScan(t, database, started["scan_id"])
|
||||
histW := httptest.NewRecorder()
|
||||
h.History(histW, httptest.NewRequest(http.MethodGet, "/api/v1/recon/history?host=hist.lab", nil))
|
||||
if histW.Code != http.StatusOK {
|
||||
t.Fatalf("history %d", histW.Code)
|
||||
}
|
||||
exportW := httptest.NewRecorder()
|
||||
h.Export(exportW, withScanIDParam(httptest.NewRequest(http.MethodGet, "/api/v1/recon/export/"+started["scan_id"]+"?format=pdf", nil), started["scan_id"]))
|
||||
if exportW.Code != http.StatusOK {
|
||||
t.Fatalf("export %d %s", exportW.Code, exportW.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user