Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,6 @@
- [WebSocket Attacks](pentesting-web/websocket-attacks.md)
- [Web Tool - WFuzz](pentesting-web/web-tool-wfuzz.md)
- [XPATH injection](pentesting-web/xpath-injection.md)
- [XS Search](pentesting-web/xs-search.md)
- [XSLT Server Side Injection (Extensible Stylesheet Language Transformations)](pentesting-web/xslt-server-side-injection-extensible-stylesheet-language-transformations.md)
- [XXE - XEE - XML External Entity](pentesting-web/xxe-xee-xml-external-entity.md)
- [XSS (Cross Site Scripting)](pentesting-web/xss-cross-site-scripting/README.md)
Expand Down
42 changes: 20 additions & 22 deletions src/pentesting-web/postmessage-vulnerabilities/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ If the **wildcard** is used, **messages could be sent to any domain**, and will

### Attacking iframe & wildcard in **targetOrigin**

As explained in [**this report**](https://blog.geekycat.in/google-vrp-hijacking-your-screenshots/) if you find a page that can be **iframed** (no `X-Frame-Header` protection) and that is **sending sensitive** message via **postMessage** using a **wildcard** (\*), you can **modify** the **origin** of the **iframe** and **leak** the **sensitive** message to a domain controlled by you.\
Note that if the page can be iframed but the **targetOrigin** is **set to a URL and not to a wildcard**, this **trick won't work**.<sup>[[9]](#references)</sup>
As explained in [**this report**](https://blog.geekycat.in/google-vrp-hijacking-your-screenshots/) if you find a page that can be **iframed** (no `X-Frame-Header` protection) and that is **sending sensitive** message via **postMessage** using a **wildcard** (\*), you can **modify** the **origin** of the **iframe** and **leak** the **sensitive** message to a domain controlled by you.<sup>[[3]](#references)</sup>\
Note that if the page can be iframed but the **targetOrigin** is **set to a URL and not to a wildcard**, this **trick won't work**.<sup>[[3]](#references)</sup>

```html
<html>
Expand Down Expand Up @@ -79,7 +79,7 @@ Note in this case how the **first thing** that the code is doing is **checking t

In order to **find event listeners** in the current page you can:

- **Search** the JS code for `window.addEventListener` and `$(window).on` (_JQuery version_)
- **Search** the JS code for `window.addEventListener` and `$(window).on` (_JQuery version_)<sup>[[5]](#references)</sup>
- **Execute** in the developer tools console: `getEventListeners(window)`

![addEventListener exploitation - Enumeration: Execute in the developer tools console: getEventListeners(window)](<../../images/image (618) (1).png>)
Expand Down Expand Up @@ -132,13 +132,13 @@ In order to **find event listeners** in the current page you can:

If a receiver only checks **`event.origin`** (e.g., trusts any `*.trusted.com`) you can often find a **"relay" page on that origin that echoes attacker-controlled params via `postMessage`** to a supplied `targetOrigin`/`targetWindow`. Examples include marketing/analytics gadgets that take query params and forward `{msg_type, access_token, ...}` to `opener`/`parent`. You can:

- **Open the victim page in a popup/iframe that has an `opener`** so its handlers register (many pixels/SDKs only attach listeners when `window.opener` exists).
- **Open the victim page in a popup/iframe that has an `opener`** so its handlers register (many pixels/SDKs only attach listeners when `window.opener` exists).<sup>[[4]](#references)</sup>
- **Navigate another attacker window to the relay endpoint on the trusted origin**, populating message fields you want injected (message type, tokens, nonces).
- Because the message now comes **from the trusted origin**, origin-only validation passes and you can trigger privileged behaviors (state changes, API calls, DOM writes) in the victim listener.

Abuse patterns seen in the wild:

- Analytics SDKs (e.g., pixel/fbevents-style) consume messages like `FACEBOOK_IWL_BOOTSTRAP`, then **call backend APIs using a token supplied in the message** and include **`location.href` / `document.referrer`** in the request body. If you supply your own token, you can **read these requests in the token’s request history/logs** and exfil **OAuth codes/tokens** present in the URL/referrer of the victim page.<sup>[[3]](#references)</sup>
- Analytics SDKs (e.g., pixel/fbevents-style) consume messages like `FACEBOOK_IWL_BOOTSTRAP`, then **call backend APIs using a token supplied in the message** and include **`location.href` / `document.referrer`** in the request body. If you supply your own token, you can **read these requests in the token’s request history/logs** and exfil **OAuth codes/tokens** present in the URL/referrer of the victim page.
- Any relay that reflects arbitrary fields into `postMessage` lets you **spoof message types** expected by privileged listeners. Combine with weak input validation to reach Graph/REST calls, feature unlocks, or CSRF-equivalent flows.

Hunting tips: enumerate `postMessage` listeners that only check `event.origin`, then look for **same-origin HTML/JS endpoints that forward URL params via `postMessage`** (marketing previews, login popups, OAuth error pages). Stitch both together with `window.open()` + `postMessage` to bypass origin checks.
Expand Down Expand Up @@ -181,7 +181,7 @@ bypassing-sop-with-iframes-2.md
### X-Frame-Header bypass

In order to perform these attacks ideally you will be able to **put the victim web page** inside an `iframe`. But some headers like `X-Frame-Header` can **prevent** that **behaviour**.\
In those scenarios you can still use a less stealthy attack. You can open a new tab to the vulnerable web application and communicate with it:
In those scenarios you can still use a less stealthy attack. You can open a new tab to the vulnerable web application and communicate with it:<sup>[[2]](#references)</sup>

```html
<script>
Expand Down Expand Up @@ -249,7 +249,7 @@ For **more information**:

### Origin-derived script loading & supply-chain pivot (CAPIG case study)

`capig-events.js` only registered a `message` handler when `window.opener` existed. On `IWL_BOOTSTRAP` it checked `pixel_id` but stored `event.origin` and later used it to build `${host}/sdk/${pixel_id}/iwl.js`.<sup>[[5]](#references)</sup>
`capig-events.js` only registered a `message` handler when `window.opener` existed. On `IWL_BOOTSTRAP` it checked `pixel_id` but stored `event.origin` and later used it to build `${host}/sdk/${pixel_id}/iwl.js`.<sup>[[6]](#references)</sup>

<details>
<summary>Handler writing attacker-controlled origin</summary>
Expand Down Expand Up @@ -287,7 +287,7 @@ If direct opener control was impossible, compromising a third-party iframe on th

### 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:
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:<sup>[[7]](#references)</sup>

```javascript
// Parent (trusted page)
Expand All @@ -314,7 +314,7 @@ postMessage({
}, "*")
```

3. 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.<sup>[[6]](#references)</sup>
3. 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:

Expand All @@ -324,12 +324,12 @@ Key takeaways:

### 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:<sup>[[7]](#references)</sup>
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:<sup>[[8]](#references)[[9]](#references)</sup>

- **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:** Some SDKs expose a reinit path; in the FB SDK, firing `init:post` with `{xfbml:1}` forces `XFBML.parse()`, destroys/recreates the plugin iframe, and generates new names/callback IDs. Repeated reinit produces as many PRNG outputs as needed (note extra internal `Math.random()` calls for callback/iframe IDs, so solvers must skip intervening values).
- **Trusted-origin delivery via parameter pollution:** If a first-party plugin endpoint reflects an unsanitized parameter into the cross-window payload (e.g., `/plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET`), you can inject `&type=...&iconSVG=...` while preserving the trusted `facebook.com` origin.
- **Predict the next callback:** Convert leaked iframe names back to floats in `[0,1)` and feed several values (even non-consecutive) into a V8 `Math.random` predictor (e.g., Z3-based). Generate the next `guid()` locally to forge the expected callback token.<sup>[[8]](#references)</sup>
- **Predict the next callback:** Convert leaked iframe names back to floats in `[0,1)` and feed several values (even non-consecutive) into a V8 `Math.random` predictor (e.g., Z3-based). Generate the next `guid()` locally to forge the expected callback token.
- **Trigger the sink:** Craft the postMessage data so the bridge dispatches `xd.mpn.setupIconIframe` and injects HTML in `iconSVG` (e.g., URL-encoded `<img src=x onerror=...>`), achieving DOM XSS inside the hosting origin; from there, same-origin iframes (OAuth dialogs, arbiters, etc.) can be read.
- **Framing quirks help:** The chain requires framing. In some mobile webviews, `X-Frame-Options` may degrade to unsupported `ALLOW-FROM` when `frame-ancestors` is present, and “compat” parameters can force permissive `frame-ancestors`, enabling the `window.name` side channel.

Expand All @@ -348,16 +348,14 @@ iframe.location = fbMsg // sends postMessage from facebook.com with forged callb

## References

- [1] [DOM XSS via PostMessage (jlajara)](https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html)
- [2] [How to spot and exploit postMessage vulnerabilities](https://dev.to/karanbamal/how-to-spot-and-exploit-postmessage-vulnerablities-36cd)
- [3] [Leaking fbevents: OAuth code exfiltration via postMessage trust leading to Instagram ATO](https://ysamm.com/uncategorized/2026/01/16/leaking-fbevents-ato.html)
- [4] [eventlistener-xss-recon (practice lab)](https://github.com/yavolo/eventlistener-xss-recon)
- [5] [CAPIG postMessage origin trust → script loading + stored JS injection](https://ysamm.com/uncategorized/2025/01/13/capig-xss.html)
- [6] [Self XSS Facebook Payments](https://ysamm.com/uncategorized/2026/01/15/self-xss-facebook-payments.html)
- [7] [Facebook JavaScript SDK Math.random callback prediction → DOM XSS writeup](https://ysamm.com/uncategorized/2026/01/17/math-random-facebook-sdk.html)
- [8] [V8 Math.random() state recovery (Z3 predictor)](https://github.com/PwnFunction/v8-randomness-predictor)
- [9] [Google VRP: Hijacking your screenshots](https://blog.geekycat.in/google-vrp-hijacking-your-screenshots/)
- [1] [DOM XSS PostMessage - jlajara](https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html)
- [2] [How to spot and exploit postMessage vulnerabilities - karanbamal](https://dev.to/karanbamal/how-to-spot-and-exploit-postmessage-vulnerablities-36cd)
- [3] [Google VRP: Hijacking your screenshots - GeekyCat](https://blog.geekycat.in/google-vrp-hijacking-your-screenshots/)
- [4] [Leaking fbevents: OAuth code exfiltration via postMessage trust leading to Instagram ATO](https://ysamm.com/uncategorized/2026/01/16/leaking-fbevents-ato.html)
- [5] [eventlistener-xss-recon (practice)](https://github.com/yavolo/eventlistener-xss-recon)
- [6] [CAPIG postMessage origin trust → script loading + stored JS injection](https://ysamm.com/uncategorized/2025/01/13/capig-xss.html)
- [7] [Self XSS Facebook Payments](https://ysamm.com/uncategorized/2026/01/15/self-xss-facebook-payments.html)
- [8] [Facebook JavaScript SDK Math.random callback prediction → DOM XSS writeup](https://ysamm.com/uncategorized/2026/01/17/math-random-facebook-sdk.html)
- [9] [V8 Math.random() state recovery (Z3 predictor)](https://github.com/PwnFunction/v8-randomness-predictor)

{{#include ../../banners/hacktricks-training.md}}


Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ victim.postMessage(buffer, "*", [buffer.buffer])

Passing the `ArrayBuffer` in the **transfer list** is useful here: ownership moves to the victim, so the sender usually avoids paying the full copy/clone cost locally.

Recent Postviewer variants showed that **any attacker-controlled synchronous work reachable from the parent's `message` handler** can be enough.<sup>[[2]](#references)</sup> Examples worth hunting for are loops over attacker-controlled lengths or debug leftovers such as:
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:

```javascript
window.onmessage = (e) => {
Expand All @@ -71,7 +71,7 @@ window.onmessage = (e) => {
}
```

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](../xs-search.md#busy-event-loop), but here the goal is to arm the malicious child before the parent resumes.
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](../xs-search/README.md#busy-event-loop), but here the goal is to arm the malicious child before the parent resumes.

### Timing the race

Expand All @@ -84,7 +84,7 @@ The race window is usually only a few milliseconds, so use cheap synchronization

### Popup / non-frameable variant

A useful 2025 evolution of the same idea appeared in **Postviewer v5²**.<sup>[[2]](#references)</sup> 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:
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>

```html
<script>
Expand All @@ -108,7 +108,7 @@ This is handy when you only control a `window.open()` flow, or when frame restri

## References

- [1] [Postviewer - writeup](https://gist.github.com/terjanq/7c1a71b83db5e02253c218765f96a710)
- [2] [Postviewer v5² Writeup](https://gist.github.com/terjanq/e66c2843b5b73aa48405b72f4751d5f8)
- [1] [Terjanq writeup - Winning RCs with Iframes](https://gist.github.com/terjanq/7c1a71b83db5e02253c218765f96a710)
- [2] [Terjanq writeup - Postviewer v5² (Google CTF 2025)](https://gist.github.com/terjanq/e66c2843b5b73aa48405b72f4751d5f8)

{{#include ../../banners/hacktricks-training.md}}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Iframes in SOP-1

In this [**challenge**](https://github.com/terjanq/same-origin-xss) created by [**NDevTK**](https://github.com/NDevTK) and [**Terjanq**](https://github.com/terjanq) you need you need to exploit a XSS in the coded
In this [**challenge**](https://github.com/terjanq/same-origin-xss) created by [**NDevTK**](https://github.com/NDevTK) and [**Terjanq**](https://github.com/terjanq) you need you need to exploit a XSS in the coded<sup>[[1]](#references)</sup>

```javascript
const identifier = "4a600cd2d4f9aa1cfb5aa786"
Expand Down Expand Up @@ -75,9 +75,6 @@ That **payload** will get the **identifier** and send a **XSS** it **back to the

## References

- [1] [same-origin-xss challenge (NDevTK & Terjanq)](https://github.com/terjanq/same-origin-xss)
- [1] [soXSS - writeup](https://github.com/terjanq/same-origin-xss)

{{#include ../../banners/hacktricks-training.md}}



Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,3 @@ Combine this with the DOM-clobbering trick above: once the receiver only sees `e
- [4] [obligatory-calc final solution (calc.html) by @terjanq](https://gist.github.com/terjanq/0bc49a8ef52b0e896fca1ceb6ca6b00e#file-calc-html)

{{#include ../../banners/hacktricks-training.md}}



Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ This is specially useful in **postMessages** because if a page is sending sensit

## References

- [1] [Hijacking your screenshots - Google VRP writeup](https://blog.geekycat.in/google-vrp-hijacking-your-screenshots/)
- [1] [Google VRP: Hijacking Your Screenshots](https://blog.geekycat.in/google-vrp-hijacking-your-screenshots/)

{{#include ../../banners/hacktricks-training.md}}
Loading