-15% na nową książkę sekuraka: Wprowadzenie do bezpieczeństwa IT. Przy zamówieniu podaj kod: 10000

Marginwidth/marginheight – nieoczekiwany sposób komunikacji międzydomenowej

13 lipca 2020, 23:00 | Teksty | komentarze 4

6 lipca 2020 na swoim Twitterze wrzuciłem wyzwanie XSS-owe. Tylko czterem osobom udało się je rozwiązać i – co ciekawe – wszyscy powiedzieli mi, że nie słyszeli wcześniej o sztuczce, jakiej trzeba było użyć do rozwiązania zadania. Postanowiłem więc opisać to zadania wraz z historią jak na to natrafiłem.

Kluczową częścią tego zadania były następujące linie w JS:

document.addEventListener("DOMContentLoaded", () => {
    for (let attr of document.body.attributes) {
        eval(attr.value);
    }
});

W kodzie mamy iterację po wszystkich atrybutach elementu <body> i wykonanie ich wartości jako kod JS. Nie ma w zadaniu innych punktów wejścia, więc rozwiązanie musiało polegać na możliwości wstrzyknięcia dowolnej wartości atrybutu w document.body. Jak więc jest to możliwe?

Wszystko zaczęło się, gdy zauważyłem ciekawy fragment w specyfikacji HTML-a. W rozdziale czternastym, nazwanym „Rendering”, opisane są między innymi domyślne style elementów w HTML-u. Na przykład, <style> i <script> domyślnie mają styl display:none. Ciekawe było jednak to w jaki sposób ustalany jest atrybut margin dla elementu <body>:

Z tabeli wynika, że jeżeli element <body> ma atrybut marginheight to jest on m mapowany do właściwości margin-top z CSS-ów. Jeśli nie istnieje, to przeglądarka patrzy na atrybut topmargin. Jeśli i ten nie istnieje, to zaczyna się element zaskoczenia: jeśli obecna strona znajduje się w zagnieżdżonym kontekście przeglądarki (czyli w elemencie <frame> lub <iframe>), przeglądarka patrzy na atrybut marginwidth z elementu kontenera. Co ciekawe – to działa pomiędzy różnymi domenami, o czym specyfikacja mówi wprost:

Na początku pomyślałem, że to jakaś zaszłość historyczna – i żadna przeglądarka już tak dzisiaj nie działa.

Zachowanie przeglądarek

Jednak żeby sprawdzić, napisałem sobie krótki kod pozwalający to zweryfikować:

<iframe src="https://sekurak.pl/.htaccess" marginwidth="100px"></iframe>

Chromium

W Chromium atrybut marginwidth został skopiowany do elementu body z zastrzeżeniem, że wcześniej został zrzutowany na wartość liczbową (100px zamieniło się na 100). Co ciekawe, Chromium nasłuchuje na zmiany wartości marginwidth z <iframe> i wartość w <body> wewnątrz ramki zmienia się dynamicznie, jeśli zmieni się też wartość z <iframe>.

<style>
  iframe, input {
    width:400px;
  }
</style>
<iframe id=ifr src="https://sekurak.pl/.htaccess" marginwidth="0"></iframe>
<br>
<input type=range 
       min=0 
       max=500 
       value=0
       oninput="ifr.setAttribute('marginwidth', this.value)">

Firefox

W Firefoksie wartość <iframe marginwidth> nie jest kopiowana jako atrybut do drzewa DOM. Ale jest jednak brana pod uwagę przy przeliczaniu stylu i można użyć funkcji getComputedStyle() by ją pobrać. Powyższy przykład z dynamiczną zmianą wartości zadziałałby więc dokładnie tak samo jak w Chromium.

Safari

W Safari wartość <iframe marginwidth> jest odbijana w zagnieżdżonym <body> bez jakiejkolwiek modyfikacji.

W przeciwieństwie do Firefoksa i Chromium, Safari nie nasłuchuje na zmiany wartości.

Rozwiązanie zadania

Mając na uwadze powyższe, rozwiązaniem zadania jest po prostu:

<iframe src="https://securitymb.github.io/xss/3"
        marginwidth="alert(document.domain)">

Gratulacje dla  @terjanq@shafigullin@BenHayak i @steike za znalezienie oczekiwanego rozwiązania!

Jeśli ktoś próbował się zmierzyć z zadaniem, ale nie udało się znaleźć rozwiązania, to podpowiedź była w opisie, gdzie znajdował się tekst: „it might be marginally better to use Safari” ;)

Marginwidth/marginheight jako międzydomenowy kanał komunikacyjny

Ciekawym efektem ubocznym marginwidth/marginheight jest fakt, że może zostać wykorzystany jako kanał komunikacji pomiędzy różnymi domenami. Da się to zrobić w każdej przeglądarce:

  • W Safari ustawiamy marginwidth z poziomu rodzica i z poziomu zagnieżdżonej ramki czytamy po prostu marginwidth z <body>,
  • W Chrome, ustawiamy marginwidth bajt po bajcie z poziomu rodzica i z poziomu zagnieżdżonej ramki nasłuchujemy na zmiany atrybutu <body marginwidth>,
  • W Firefoksie, ustawiamy marginwidth bajt po bajcie z poziomu rodzica i weryfikujemy regularnie wartość zwracaną przez getComputedStyle(document.body).marginLeft z poziomu zagnieżdżonej ramki.

Zaimplementowałem powyższą logikę i zahostowałem na https://cdn.sekurak.pl/marginwidth.html:

Podsumowanie

Jak dla mnie najciekawszym wnioskiem z powyższych rozważań jest fakt, że w specyfikacji HTML nadal można znaleźć interesujące fragmenty, które pomagają w pewnych rzadkich atakach.

Dodatkowo, marginwidth prawdopodobnie ma potencjał na atak XS-Leaks, choć mi nie udało się znaleźć żadnego działającego scenariusza.

— Michał Bentkowski (@SecurityMB), prowadzi szkolenia dla programistów i testerów w Securitum

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



Komentarze

  1. Jan

    Niezłe, muszę przetestować.

    Odpowiedz
  2. Tomek

    I dlatego lubię Was czytać 😁

    Odpowiedz
  3. Tomek S.

    Żeby było mniej podejrzanie można ustawiać najmniej znaczące bity tych wartości, tak jak to robią jakieś narzędzia steganograficzne do kodowania informacji w obrazkach np.:

    https://www.openstego.com/
    https://www.pelock.com/products/steganography-online-codec

    Potem wystarczy taki stream bitów poskładać „do kupy”, aby odtworzyć całościową wiadomość.

    Już widzę komunikator :-D

    Margin Messenger :-D

    Odpowiedz
  4. AS

    A ja się zawsze męczyłem i po stronie PHP się komunikowałem, zamiast to na marginesie między ramkami poprzesyłać ;-)

    Odpowiedz

Odpowiedz