fix(scan): percent-encode dork queries before search (#327)

the google-search client only swaps spaces for '+' and drops the term
into the query string verbatim, so a dork carrying a raw '#' or '&' cut
the request url short at the fragment or split it into stray query
params. the search then ran against a fragment of the intended dork.
encode the term up front so the whole dork survives.
This commit is contained in:
Tigah
2026-07-22 12:53:04 -07:00
committed by GitHub
parent a35f3bc71f
commit 1f6d0f7cf0
2 changed files with 95 additions and 5 deletions
+12 -5
View File
@@ -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
+83
View File
@@ -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)
}
}