Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
XSS-owanie aplikacji Google dla zabawy i dolarów
Wstęp
Półtora roku temu opisywałem na Sekuraku trzy bardzo podobne do siebie XSS-y, które znalazłem w Google Caja. Okazuje się, że projekt jest dla mnie wyjątkowo obfity w błędy, udało mi się bowiem zlokalizować w nim kolejne trzy ucieczki z sandboksa – które w rzeczywistych aplikacjach mogłyby prowadzić do XSS-ów.
Przypomnijmy sobie zatem, czym właściwie jest Caja. Google Caja (z hiszpańskiego słowa oznaczającego pudełko, czytane /kaha/) jest projektem, który w założeniu ma pozwolić twórcom aplikacji internetowych na bezpieczne umieszczanie kodu HTML, JavaScript czy CSS pochodzącego z niezaufanych źródeł. Czyli na przykład chcemy użytkownikowi pozwolić na personalizację naszej strony z użyciem JavaScriptu, ale nie powinien on mieć dostępu do drzewa DOM, a zatem nie powinien być w stanie dostać się do poufnych danych przechowywanych na stronach.
Caja realizuje ten cel poprzez wprowadzenie pewnego rodzaju sandboksa zarówno na warstwie HTML, jak i JavaScript. Dawniej Caja była wykorzystywana m.in. w Google Docs (o tym pisałem też rok temu), natomiast obecnie w tej aplikacji wykorzystywane jest sandboksowanie z użyciem elementów iframe. Nie zmienia to jednak faktu, że Caja nadal jest w niektórych miejscach przez Google używana, a co za tym idzie jest wartościowym celem do analiz pod kątem bezpieczeństwa.
Jak testować Caję
Twórcy Cajy udostępniają specjalne środowisko do testów pod adresem http://caja.appspot.com/ (Rys 1). Możemy w nim wpisać fragment kodu HTML, następnie użyć przycisku „Cajole” i zobaczyć, w jaki sposób kod będzie wykonywał po przetworzeniu go przez Caję.
Spróbujmy w najprostszym przypadku użyć tradycyjnego, „XSS-owego” kawałku kodu:
<script> alert(1) </script>
Po naciśnięciu przycisku Cajole, zobaczymy oczywiście alert (Rys 2).
Zwróćmy jednak uwagę, że w naszym alercie nie wyświetliła się po prostu jedynka, ale została ona poprzedzona tekstem „Untrusted code says: „. Dzieje się tak, ponieważ Caja w tym środowisku testowym nadpisuje oryginalną funkcję alert swoją własną, która dopisuje te trzy słowa na początku.
Takie zachowanie tego środowiska jest z punktu widzenia naszych testów bardzo pomocne: oznacza bowiem, że jeśli uda nam się wykonać taki alert, który nie zostanie poprzedzony tekstem „Untrusted code says: „, to udało nam się uciec z sandboksa.
Jak działa Caja
W języku JavaScript istnieje wiele sposobów na wykonanie kodu podając go jako string. Najprostszym i najbardziej znanym przykładem jest funkcja eval, ale poza nią istnieje jeszcze np. konstruktor Function. Aby dostać się do tego konstruktora, możemy w JS albo odwołać się do globalnego obiektu Function, albo do właściwości constructor dowolnej funkcji (Rys 3).
Mając dostęp do konstruktora Function możemy wykonać dowolny kod JS przekazując go jako string. Co ważne – samo wywołanie Function po prostu zwraca nam funkcję, więc później musimy jeszcze ją wywołać (Rys 4).
Wywołanie funkcji przez konstruktor Function gwarantuje nam, że kod jest wywoływany zawsze na globalnej przestrzeni nazw w obecnym kodzie JS (gwarantuje to nam standard ECMAScript).
Caja, by uniemożliwić wykorzystanie konstruktora Function do wykonania dowolnego kodu, napisuje go swoim własnym, „fałszywym” konstruktorem, w którym dba o to, by kod został odpowiednio wyizolowany (Rys 5).
Standard ECMAScript 6 wprowadził nowy typ funkcji – generatory (programistom innych języków programowania, np. Pythona, ten mechanizm powinien już być znany). W kodzie możemy je zdefiniować, dodając znak gwiazdki po słowie kluczowym function, np.
function* generator() { yield 1; yield 2; }
Caja, pomimo że nie wspiera de facto nowej składni ECMAScript 6, w ramach lepszego zabezpieczenia środowiska podstawia również „fałszywy” konstruktor do generatora, by uniemożliwić takie wyjście z sandboksa (Rys 6).
Nowinki w ECMAScript, cz. 1
Standard ECMAScript rozwija się w ostatnim czasie jednak niezwykle dynamicznie, dość często w przeglądarkach dostajemy różne nowe konstrukcje składniowe lub mechanizmy. Jednym z nich, chyba największym w ostatnich paru miesiącach, są funkcje asynchroniczne (async/await) dzięki którym możemy pisać kod asynchroniczny w taki sposób, że wygląda niemal jak kod synchroniczny. Na przykład dzięki funkcjom asynchronicznym możemy poniższy kod, pobierający zawartość Sekuraka i wypisujący go w alercie:
fetch("https://sekurak.pl") .then(r => r.text()) .then(text => alert(text));
zastąpić o wiele ładniejszym kodem, wyglądającym praktycznie jak kod synchroniczny:
async function get() { const resp = await fetch("https://sekurak.pl"); alert(await resp.text()); }
Przy okazji analizy tematu wykradania danych za pomocą CSS-ów, czytałem więcej o funkcjach asynchronicznych w dokumentacji MDN i natychmiast rzucił mi się wówczas w oczy fakt, że funkcje asynchroniczne mają nowy konstruktor! Ponieważ Caja nie była aktualizowana od dłuższego czasu, podejrzewałem, że jest spora szansa, że będę mógł użyć tego sposobu do ucieczki z sandboksa.
Wszedłem zatem szybko na https://caja.appspot.com i wpisałem poniższy kod:
<script> <!-- (async function(){}).constructor('alert(1)')() </script>
Po wykonaniu takiego kodu wyświetlił się alert niepoprzedzony tekstem „Untrusted code says:”, co oznaczało, ze rzeczywiście mogłem uciec z sandboksa (Rys 7).
Nie pozostało mi w tym momencie nic innego jak zgłoszenie błędu do Google i – niedługo później – wypicie kieliszka wina z okazji przyznanego bounty ;-)
Nowinki w ECMAScript, cz. 2
Kilka dni po zgłoszeniu błędu pomyślałem, że może powinienem spróbować pójść za ciosem – bo skoro stosunkowo nowa cecha JavaScriptu umożliwiła mi ucieczkę z piaskownicy, to może są jeszcze jakieś inne, dzięki którym będę mógł zrobić to samo?
Zajrzałem więc na listę ECMAScript compatibility table, na której możemy sobie podejrzeć „ficzery” wprowadzane w konkretnych wersjach standardu. Mnie interesowały w szczególności te najnowsze, które są w fazie trzeciej.
Szczególnie ciekawie wyglądał feature „Asynchronous iterators„, który de facto po prostu wprowadza asynchroniczne generatory. Podobnie jak przy synchronicznych generatorach, by użyć takich generatorów, wystarczy dodać znak gwiazdki po słowie function. Wracam więc jeszcze raz na https://caja.appspot.com i wpisuję:
<script> <!-- (async function*(){})['constructor']('alert(1)')().next(); </script>
Na końcu kodu musiałem jeszcze dodać wywołanie metody next(); inaczej bowiem generator nie zaczyna się wykonywać. Efekt był taki, że po raz kolejny zobaczyłem niesandoksowany alert!
Zgłosiłem nowy sposób ucieczki z sandboksa do zespołu bezpieczeństwa Google i – prawdę powiedziawszy – spodziewałem się, że ten błąd zostanie zaliczony na poczet tego poprzedniego ze względu na ich podobieństwo. Ku mojemu miłemu zaskoczeniu, Google zdecydowało przyznać drugie bounty w normalnej wysokości.
Nowinki w ECMAScript, cz. 3
Chcąc iść dalej za ciosem przejrzałem wszystkie nowe funkcje wypisane w „ECMAScript Compatilibity Table”, ale żadnej z nich nie udało mi się już wykorzystać do ucieczki z sandboksa Cajy. Zanosiło się zatem, że moje szczęście do szukania błędów w tym projekcie już się skończyło.
Po niecałym miesiącu okazało się, że sam popełniłem błąd, bo zamiast spojrzeć do oficjalnych źródeł z listą propozycji do standardu ECMAScript, zaglądałem na tę tabelkę kompatybilności, która nie zawierała ich wszystkich!
Źródło z kolei powiedziało mi, że do standardu wprowadzane są dynamiczne importy, a więc możliwość dołączenia innego pliku JS z poziomu istniejącego kodu JS. W praktyce wygląda to na przykład tak:
<script> const rand = parseInt(Math.random() * 5); const modules = ['dog', 'cat', 'frog', 'rhino', 'duck']; const selectedModule = modules[rand]; // Zaimportuj plik JS w zależności od wylosowanego zwierzęcia import(`http://animal-farm.local/js/${selectedModule}.js`); </script>
Ponieważ dzięki importom jesteśmy w stanie wykonać całkowicie zewnętrzny kod JS, wydawało mi sie bardzo prawdopodobne, że Caja nie będzie w stanie takiego kodu odpowiednie sandboksować. Spróbowałem więc poniższego kodu:
<script> <!-- import('data:application/javascript,alert(1)'); </script>
I okazało się, że rzeczywiście w tym przypadku wykonał się niesandboksowany alert!
Wszystkie trzy exploity naraz
Aby zaprezentować wszystkie trzy exploity naraz możemy posłużyć się poniższym kodem:
<script> <!-- (async function(){}).constructor('alert(1)')(); <!-- (async function*(){})['constructor']('alert(2)')().next(); <!-- import('data:application/javascript,alert(3)'); </script>
Podsumowanie
W tekście opisałem trzy XSS-y (a właściwie ucieczki z sandboksa) w projekcie Google Caja. Wszystkie z nich były wykonalne dzięki użyciu nowych cech standardu ECMAScript, które były wdrażane w przeglądarkach w tym roku.
Pokazuje to, że warto wiedzieć co w standardzie ECMAScript piszczy nie tylko po to, by pisać ładniejsze/lepsze/bardziej zwięzłe aplikacji, ale także po to, by móc przeprowadzać ataki, które wcześniej nie były możliwe.
Twórcy Cajy wydali oficjalne security advisory dotyczące tych błędów.
Michał Bentkowski, na co dzień hakuje w Securitum
Timeline
- 28.08.2017 – Zgłoszenie do Google błędu z funkcjami asynchroniczymi,
- 28.08.2017 – Potwierdzenie błędu przez Google,
- 02.09.2017 – Zgłoszenie do Google błędu z generatorami asynchronicznymi,
- 04.09.2017 – Potwierdzenie błędu przez Google,
- 19.09.2017 – Przyznanie bounty za oba błędy,
- 29.09.2017 – Zgłoszenie do Google błędu z dynamicznymi importami,
- 02.10.2017 – Potwierdzenie błędu przez Google,
- 10.10.2017 – Przyznanie bounty za dynamiczne importy,
- 19.11.2017 – Pytanie do Google jaki jest status zgłoszonych błędów, bo wyglądają na nadal nienaprawione,
- 27.11.2017 – Sprawdzenie przez Google sytuacji – okazało się, że błędy zostały naprawione ale poprawki nie zostały wypuszczone na świat. W tym samym dniu caja.appspot.com została zaktualizowana.
- 30.11.2017 – Tekst na Sekuraku.
chciałem się pobawić jednak nie jest to wcale proste, brakuje poradnika czy jakiegoś prostego tutka, co i jak
Mój AVG na Win10 blokuje tą stronkę z powodu „JS:CVE-2016-3198-A” . Pomijam fakt, że jest to błąd w MS Edge a wyświetlałem na FF i Chrome. Ciekawi mnie który element mógł spowodować ten alarm, bo nie udało mi się znaleźć :)
Ciekawe czy JS umieszczone w SVG w CSS background lub content przejdzie.
Super tekst, jak zwykle :)
Dzieki :)
1. czy taki laik jak ja, nie majacy pojecia o programowaniu etc., ma szanse by jakims poradnikiem zaczac szukac i zglaszac luki ?
2. co jest wymagane: jaki sprzet, programy, konta dev ?, konta w banku ? itd.
3. moze wreszcie zrobie upgrade mojego 10letniego kompa :0
na początek rób ctfy i zadanka hackme tu masz listę przydatnych stronek http://gynvael.coldwind.pl/?id=366
W jaki sposób podejrzałeś konstruktory, którymi Caja nadpisuje te wbudowane?
„`(function(){}).constructor„` zwraca w konsoli:
„`ƒ Function() { [native code] }„`