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

30 listopada 2017, 09:22 | Teksty | komentarzy 7
W niniejszym artykule opisuję kolejne trzy błędy typu XSS, które zgłosiłem do Google w ramach programu bug bounty. Podobnie jak w zeszłym roku, XSS-y występowały w projekcie Google Caja.

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ę.

Rys 1. Środowisko testowe caja.appspot.com

Rys 1. Środowisko testowe caja.appspot.com

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).

Rys 2. Wykonanie funkcji alert(1)

Rys 2. Wykonanie funkcji alert(1)

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).

Rys 3. Odwołanie się do konstruktora Function

Rys 3. Odwołanie się do konstruktora Function

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).

Rys 4. Przykład użycia konstruktora Function

Rys 4. Przykład użycia konstruktora Function

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).

Rys 5. Caja nadpisuje konstruktor funkcji swoim własnym - FakeFunction

Rys 5. Caja nadpisuje konstruktor funkcji swoim własnym – FakeFunction

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).

Rys 6. Fałszywy generator (FakeGeneratorFunction) wystawiany przez Caję

Rys 6. Fałszywy generator (FakeGeneratorFunction) wystawiany przez Caję

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>​
Wyjaśnienie dlaczego musiałem w pierwszej linii skryptu dodać znaki „<!–” znajdziecie, wraz z fragmentem kodu źródłowego Cajy, w poprzednim tekście o XSS-ach w Google Caja.

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).

Rys 7. Alert udowadaniający wyjście z sandboksa.

Rys 7. Alert udowadaniający wyjście z sandboksa.

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.

W standardzie ECMAScript faza trzecia oznacza, że dana cecha języka jest już bardzo bliska oficjalnego dodania do standardu, ale wymaga jeszcze ostatnich szlifów. Wiele z nich na tym etapie jest już implementowanych przez przeglądarki w wersjach deweloperskich lub mogą być używane po włączeniu odpowiedniej flagi w przeglądarce.

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.

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



Komentarze

  1. aaa

    chciałem się pobawić jednak nie jest to wcale proste, brakuje poradnika czy jakiegoś prostego tutka, co i jak

    Odpowiedz
  2. Bloosky

    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źć :)

    Odpowiedz
  3. Jednym słowem

    Ciekawe czy JS umieszczone w SVG w CSS background lub content przejdzie.

    Odpowiedz
  4. bl4de

    Super tekst, jak zwykle :)

    Dzieki :)

    Odpowiedz
  5. n00b

    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

    Odpowiedz
  6. Krzysztof

    W jaki sposób podejrzałeś konstruktory, którymi Caja nadpisuje te wbudowane?

    „`(function(){}).constructor„` zwraca w konsoli:
    „`ƒ Function() { [native code] }„`

    Odpowiedz

Odpowiedz