Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Marginwidth/marginheight – nieoczekiwany sposób komunikacji międzydomenowej
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 prostumarginwidth
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ą przezgetComputedStyle(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
Niezłe, muszę przetestować.
I dlatego lubię Was czytać 😁
Ż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
A ja się zawsze męczyłem i po stronie PHP się komunikowałem, zamiast to na marginesie między ramkami poprzesyłać ;-)