Mega Sekurak Hacking Party w Krakowie! 26-27.10.2026 r.
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
marginwidthz poziomu rodzica i z poziomu zagnieżdżonej ramki czytamy po prostumarginwidthz<body>, - W Chrome, ustawiamy
marginwidthbajt po bajcie z poziomu rodzica i z poziomu zagnieżdżonej ramki nasłuchujemy na zmiany atrybutu<body marginwidth>, - W Firefoksie, ustawiamy
marginwidthbajt po bajcie z poziomu rodzica i weryfikujemy regularnie wartość zwracaną przezgetComputedStyle(document.body).marginLeftz 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ć ;-)