/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · Blazing-fast pentesting suite : : ▄█ █ █▀ · BSD 3-Clause License : : : : (c) 2022-2026 vmfunc, xyzeva, : : lunchcat alumni & contributors : : : ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· */ package scan import ( "fmt" "net/http" "net/http/httptest" "testing" "time" ) // representative bodies, not padded to force a size delta: a generic homepage and a // real admin login just have different content, hence different shapes. the catch-all // body carries no db keyword, so the comparison turns on the baseline, not isAdminPanel. const ( catchAllHome = `Acme` + `
Welcome to Acme.
` realPMALogin = `` + `phpMyAdmin` + `
` + `Log in to phpMyAdmin` + `` + `` + `
` realDBAdmin = `Database Manager` + `

MySQL Server 8.0

` + `
DatabaseTables
app_production42
` + `
` + `` ) // countAdminPanels is nil-safe: SQL returns a nil result when nothing is found. func countAdminPanels(r *SQLResult) int { if r == nil { return 0 } return len(r.AdminPanels) } // hasPanelType reports whether a panel of the given type was found. func hasPanelType(r *SQLResult, panelType string) bool { if r == nil { return false } for _, p := range r.AdminPanels { if p.Type == panelType { return true } } return false } // a 200 catch-all (the SPA wildcard) is calibrated as a baseline shape. func TestCalibrateSQLBaseline_CatchAll(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(catchAllHome)) })) defer srv.Close() baselines := calibrateSQLBaseline(srv.URL, &http.Client{Timeout: 5 * time.Second}) if len(baselines) == 0 { t.Fatal("a 200 catch-all should produce at least one baseline shape") } } // a server that hard-404s every bogus path needs no baseline (status already filters). func TestCalibrateSQLBaseline_HardNotFound(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer srv.Close() baselines := calibrateSQLBaseline(srv.URL, &http.Client{Timeout: 5 * time.Second}) if len(baselines) != 0 { t.Errorf("hard-404 server should yield no baseline, got %d", len(baselines)) } } // with -ac on, a 200 catch-all serving a db-topical page at every path yields no // admin-panel finding once the wildcard shape is calibrated. func TestSQL_CatchAllSuppressed(t *testing.T) { page := "database dashboard for our service" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(page)) })) defer srv.Close() result, err := SQL(srv.URL, 5*time.Second, 4, "", true) if err != nil { t.Fatalf("SQL: %v", err) } if n := countAdminPanels(result); n != 0 { t.Errorf("catch-all produced %d admin-panel finding(s) with -ac, want 0", n) } } // a 403 WAF that blanket-blocks with a db-mentioning page is also a catch-all and // must be suppressed (covers the 403 branch of the status set). func TestSQL_403WAFCatchAllSuppressed(t *testing.T) { page := "Request blocked: possible sql injection attempt detected" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(page)) })) defer srv.Close() result, err := SQL(srv.URL, 5*time.Second, 4, "", true) if err != nil { t.Fatalf("SQL: %v", err) } if n := countAdminPanels(result); n != 0 { t.Errorf("403 WAF catch-all produced %d admin-panel finding(s) with -ac, want 0", n) } } // suppression is opt-in: with -ac off (the default) the same catch-all is still // reported, matching dirlist's behavior. func TestSQL_CalibrateDisabledStillReports(t *testing.T) { page := "database dashboard for our service" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(page)) })) defer srv.Close() result, err := SQL(srv.URL, 5*time.Second, 4, "", false) if err != nil { t.Fatalf("SQL: %v", err) } if countAdminPanels(result) == 0 { t.Error("with -ac off, the catch-all should still be reported (suppression is opt-in)") } } // a real phpMyAdmin hosted on a catch-all is still reported under -ac: a genuine // login page is a different shape than the wildcard homepage, so it is not dropped. func TestSQL_RealPanelAmongCatchAll(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/phpmyadmin/" { _, _ = w.Write([]byte(realPMALogin)) return } _, _ = w.Write([]byte(catchAllHome)) })) defer srv.Close() result, err := SQL(srv.URL, 5*time.Second, 4, "", true) if err != nil { t.Fatalf("SQL: %v", err) } if !hasPanelType(result, "phpMyAdmin") { t.Errorf("real phpMyAdmin on a catch-all should still be reported; panels=%d", countAdminPanels(result)) } } // a real generic interface (a distinct db admin page at /db/) is still reported // under -ac, so calibration does not over-suppress genuine findings. func TestSQL_RealGenericPanelAmongCatchAll(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/db/" { _, _ = w.Write([]byte(realDBAdmin)) return } _, _ = w.Write([]byte(catchAllHome)) })) defer srv.Close() result, err := SQL(srv.URL, 5*time.Second, 4, "", true) if err != nil { t.Fatalf("SQL: %v", err) } if countAdminPanels(result) == 0 { t.Error("a real database interface at /db/ on a catch-all should still be reported") } } // the normal case (no catch-all): a host that 404s everything except a real // phpMyAdmin still reports it, since calibration finds no baseline. func TestSQL_HardNotFoundRealPanelReported(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/phpmyadmin/" { _, _ = w.Write([]byte(realPMALogin)) return } w.WriteHeader(http.StatusNotFound) })) defer srv.Close() result, err := SQL(srv.URL, 5*time.Second, 4, "", true) if err != nil { t.Fatalf("SQL: %v", err) } if !hasPanelType(result, "phpMyAdmin") { t.Error("phpMyAdmin on a non-catch-all host should be reported") } } // a catch-all that reflects the request path varies its size per path; the shared // reflection-tolerant calibration suppresses it under -ac. func TestSQL_ReflectedPathCatchAllSuppressed(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "no such page: %s (database)", r.URL.Path) })) defer srv.Close() result, err := SQL(srv.URL, 5*time.Second, 4, "", true) if err != nil { t.Fatalf("SQL: %v", err) } if n := countAdminPanels(result); n != 0 { t.Errorf("reflected-path catch-all should be suppressed under -ac, got %d panel(s)", n) } } // the word-count-tolerant baseline must not nuke real findings: a genuine phpMyAdmin // hosted behind a reflecting catch-all has a distinct word count, so it still surfaces. func TestSQL_ReflectingCatchAllRealPanelStillReported(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/phpmyadmin/" { _, _ = w.Write([]byte(realPMALogin)) return } fmt.Fprintf(w, "no such page: %s (database)", r.URL.Path) })) defer srv.Close() result, err := SQL(srv.URL, 5*time.Second, 4, "", true) if err != nil { t.Fatalf("SQL: %v", err) } if !hasPanelType(result, "phpMyAdmin") { t.Errorf("real phpMyAdmin on a reflecting catch-all should still surface; panels=%d", countAdminPanels(result)) } }