diff --git a/internal/scan/dork.go b/internal/scan/dork.go index 0be05cf..e9f361b 100644 --- a/internal/scan/dork.go +++ b/internal/scan/dork.go @@ -18,8 +18,8 @@ package scan import ( "bufio" "context" - "fmt" "net/http" + "net/url" "strconv" "sync" "time" @@ -44,11 +44,18 @@ type DorkResult struct { Count int `json:"count"` // The number of times this URL appeared in the dork's results } +// dorkQuery percent-encodes the dork and host together. the google-search +// client drops the term into the query string verbatim, so an unencoded '#' +// or '&' (both common in the dork list) truncates or splits the query. +func dorkQuery(dork, host string) string { + return url.QueryEscape(dork + " " + host) +} + // Dork performs Google dorking operations on the target URL. // It uses a predefined list of dorks to search for potentially sensitive information. // // Parameters: -// - url: The target URL to dork +// - targetURL: The target URL to dork // - timeout: Maximum duration for each dork search // - threads: Number of concurrent threads to use // - logdir: Directory to store log files (empty string for no logging) @@ -56,13 +63,13 @@ type DorkResult struct { // Returns: // - []DorkResult: A slice of results from the dorking operation // - error: Any error encountered during the dorking process -func Dork(url string, timeout time.Duration, threads int, logdir string) ([]DorkResult, error) { +func Dork(targetURL string, timeout time.Duration, threads int, logdir string) ([]DorkResult, error) { output.ScanStart("URL dorking") spin := output.NewSpinner("Running Google dorks") spin.Start() - sanitizedURL := stripScheme(url) + sanitizedURL := stripScheme(targetURL) if logdir != "" { if err := logger.WriteHeader(sanitizedURL, logdir, "URL dorking"); err != nil { @@ -98,7 +105,7 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork dorkResults := []DorkResult{} pool.Each(dorks, threads, func(dork string) { - results, err := googlesearch.Search(context.TODO(), fmt.Sprintf("%s %s", dork, sanitizedURL)) + results, err := googlesearch.Search(context.TODO(), dorkQuery(dork, sanitizedURL)) if err != nil { log.Debugf("error searching for dork %s: %v", dork, err) return diff --git a/internal/scan/dork_test.go b/internal/scan/dork_test.go new file mode 100644 index 0000000..764aa26 --- /dev/null +++ b/internal/scan/dork_test.go @@ -0,0 +1,83 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package scan + +import ( + "net/url" + "strings" + "testing" +) + +// buildSearchURL mirrors how the google-search client turns a search term into +// a request url: it trims spaces, swaps the rest for '+' and drops the term +// into the query string verbatim without any encoding. +func buildSearchURL(term string) string { + term = strings.Trim(term, " ") + term = strings.ReplaceAll(term, " ", "+") + return "https://www.google.com/search?q=" + term + "&hl=en" +} + +func TestDorkQueryPreservesSpecialCharacters(t *testing.T) { + cases := []struct { + name string + dork string + }{ + {"hash", `intext:"#mysql dump" filetype:sql`}, + {"ampersand", `filetype:sql "a & b"`}, + {"question", `inurl:index.php?id=`}, + {"plain", `intitle:"index of" passwd`}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + want := tc.dork + " example.com" + + u, err := url.Parse(buildSearchURL(dorkQuery(tc.dork, "example.com"))) + if err != nil { + t.Fatalf("parse encoded url: %v", err) + } + if got := u.Query().Get("q"); got != want { + t.Errorf("encoded query = %q, want %q", got, want) + } + // a fragment or a stray query param means the dork leaked out of q. + if u.Fragment != "" { + t.Errorf("encoded url has fragment %q, query truncated", u.Fragment) + } + if len(u.Query()) != 2 { // q and hl only + t.Errorf("encoded url has stray params: %v", u.Query()) + } + }) + } +} + +// guards the regression directly: the old naive term drops everything after a +// '#' into the fragment, so the search runs against a fragment of the dork. +func TestDorkQueryFixesNaiveTruncation(t *testing.T) { + dork := `intext:"#mysql dump" filetype:sql` + + naive, err := url.Parse(buildSearchURL(dork + " example.com")) + if err != nil { + t.Fatalf("parse naive url: %v", err) + } + if naive.Query().Get("q") == dork+" example.com" { + t.Fatal("expected the naive term to truncate, but it survived") + } + + fixed, err := url.Parse(buildSearchURL(dorkQuery(dork, "example.com"))) + if err != nil { + t.Fatalf("parse fixed url: %v", err) + } + if got, want := fixed.Query().Get("q"), dork+" example.com"; got != want { + t.Errorf("fixed query = %q, want %q", got, want) + } +}