daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

blocking-main-page-to-steal-postmessage.md (6629B)


      1 ---
      2 title: "Blocking the Main Page to Steal a postMessage"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/postmessage-vulnerabilities/blocking-main-page-to-steal-postmessage.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/postmessage-vulnerabilities/blocking-main-page-to-steal-postmessage.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Blocking the Main Page to Steal a `postMessage`
     14 
     15 ## Winning RCs with Iframes
     16 
     17 According to this [**Terjanq writeup**](https://gist.github.com/terjanq/7c1a71b83db5e02253c218765f96a710), blob documents created from `null` origins can end up **process-isolated** from the parent page. This makes an interesting race possible: if you can force the **parent** window to spend enough time inside a synchronous code path, a malicious **child** document may still keep running, finish bootstrapping its JS, register `onmessage`, and steal the next sensitive `postMessage`.<sup>[[1]](#references)</sup>
     18 
     19 A simplified vulnerable flow is:
     20 
     21 ```javascript
     22 iframe.addEventListener(
     23   "load",
     24   () => {
     25     iframe.contentWindow?.postMessage(secret, "*")
     26   },
     27   { once: true }
     28 )
     29 
     30 window.addEventListener("message", (e) => {
     31   if (e.data == "blob loaded") {
     32     $("#previewModal").modal()
     33   }
     34 })
     35 ```
     36 
     37 Therefore, the goal of the attacker is to **let the parent create the iframe**, but **before** the **parent** page **sends** the sensitive data, **keep it busy** and send a **payload to the child iframe**. While the **parent** is busy, the **iframe** executes attacker-controlled JS, installs `onmessage`, and waits for the next sensitive `postMessage`. Once the parent becomes responsive again, it sends the secret and the malicious child leaks it.
     38 
     39 A practical flow is usually:
     40 
     41 1. Trigger the victim to create/load the target iframe.
     42 2. Detect when the child exists (`win.length === 1`, `frames.length > 0`, or similar heuristics).
     43 3. Send a message that reaches an **expensive synchronous gadget** in the parent.
     44 4. While the parent event loop is stalled, send your payload to the child iframe.
     45 5. Let the payload leak the next secret the parent sends to the child.
     46 
     47 ### Blocking gadgets
     48 
     49 The original 2022 challenge used a **loose comparison** gadget:<sup>[[1]](#references)</sup>
     50 
     51 ```javascript
     52 window.addEventListener("message", (e) => {
     53   if (e.data == "blob loaded") {
     54     $("#previewModal").modal()
     55   }
     56 })
     57 ```
     58 
     59 Because `==` coerces non-strings, a large `Uint8Array`/`ArrayBuffer` can make the parent spend noticeable time converting attacker-controlled data to a string:
     60 
     61 ```javascript
     62 const buffer = new Uint8Array(1e7)
     63 victim.postMessage(buffer, "*", [buffer.buffer])
     64 ```
     65 
     66 Passing the `ArrayBuffer` in the **transfer list** transfers ownership and detaches it from the sender instead of copying its contents. Whether this produces a useful delay in the receiver remains browser-, size-, and gadget-dependent.<sup>[[3]](#references)</sup>
     67 
     68 Recent Postviewer variants showed that **any attacker-controlled synchronous work reachable from the parent's `message` handler** can be enough. Examples worth hunting for are loops over attacker-controlled lengths or debug leftovers such as:
     69 
     70 ```javascript
     71 window.onmessage = (e) => {
     72   if (e.data.type === "share") {
     73     for (let i = 0; i < e.data.files.length; i++) {
     74       // expensive per-file work
     75     }
     76   }
     77 
     78   if (e.data.slow) {
     79     for (let i = 0; i < e.data.slow; i++) {}
     80   }
     81 }
     82 ```
     83 
     84 So, when auditing, don't only look for a `==` coercion gadget: also look for loops over attacker-controlled `length` fields, debug leftovers, or any other synchronous path reachable **before** the sensitive `postMessage` is sent. Conceptually this abuses the same single-thread primitive used in [busy event loop XS-Leaks](/hacktricks/pentesting-web/xs-search/overview#busy-event-loop), but here the goal is to arm the malicious child before the parent resumes.
     85 
     86 ### Timing the race
     87 
     88 The race window is usually only a few milliseconds, so use cheap synchronization signals before firing the slow gadget:
     89 
     90 - Poll for `win.length === 1` / `frames.length > 0` to know when the child exists.
     91 - Reuse a single popup/window across attempts to reduce navigation jitter.
     92 - Tune small `setTimeout` delays empirically for the browser/hardware being attacked.
     93 - If the victim uses wildcard `postMessage(..., "*")`, keep sending until the child payload is definitely installed.
     94 
     95 ### Popup / non-frameable variant
     96 
     97 A useful 2025 evolution of the same idea appeared in **Postviewer v5²**. When the target page was **not frameable**, the race was still winnable from a **popup**. Instead of directly changing `iframe.location`, the attacker used a child/popup payload that **continuously reloads itself**, creating another `onload` just before the victim cleans up its listener:<sup>[[2]](#references)</sup>
     98 
     99 ```html
    100 <script>
    101 setTimeout(() => {
    102   location = URL.createObjectURL(
    103     new Blob([document.documentElement.innerHTML], { type: "text/html" })
    104   )
    105 }, 150)
    106 </script>
    107 ```
    108 
    109 This turns the primitive into:
    110 
    111 1. Open the target in a popup.
    112 2. Get the victim to render a self-reloading attacker-controlled document.
    113 3. Render a second payload whose only job is to install `onmessage` and leak the next secret.
    114 4. Stall the opener/main page with one of the blocking gadgets above.
    115 5. When the opener resumes, it may deliver the sensitive `postMessage` to the attacker payload **before** it processes the child's cleanup/ack message.
    116 
    117 This is handy when you only control a `window.open()` flow, or when frame restrictions stop you from directly hijacking nested iframe locations.
    118 
    119 ## Defensive checks
    120 
    121 The race only matters when a sensitive message is sent to a window whose document can become attacker-controlled. Use an exact `targetOrigin` instead of `"*"`, validate both `event.origin` and `event.source` on receipt, keep untrusted input away from synchronous pre-send handlers, and re-check the destination window's expected lifecycle before releasing a secret. These controls address the trust failure even if timing changes across browser versions.<sup>[[3]](#references)</sup>
    122 
    123 ## References
    124 
    125 - [1] [Terjanq writeup - Winning RCs with Iframes](https://gist.github.com/terjanq/7c1a71b83db5e02253c218765f96a710)
    126 - [2] [Terjanq writeup - Postviewer v5² (Google CTF 2025)](https://gist.github.com/terjanq/e66c2843b5b73aa48405b72f4751d5f8)
    127 - [3] [MDN - `Window.postMessage()` security and transferable-object guidance](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)