- MonkeHacks
- Posts
- Exfiltrating Data from Sandboxed Documents
Exfiltrating Data from Sandboxed Documents
Exfiltrating Data from Sandboxed Documents
This is a writeup of a vulnerability I found with doomerhunter, that allowed us to exfiltrate data from an extremely limited Javascript environment.
The Functionality
This all began when doomerhunter saw a feature in the web application that allowed custom HTML and Javascript. The application would take the custom JS widget, and place it within an iframe
on the main dashboard. This existed to allow users to implement custom logic and CSS. Any kind of feature like this is incredibly difficult to implement correctly, so naturally, we probed further.
So, how was it doing this? Well, they were using postMessage
! The parent window (the dashboard) was sending a postMessage
message, containing the JS in a string, to the iframed document. The document endpoint had a listener that would take this data and populate the page with it. So, in a sense, this was a postMessage-based DOM XSS by design.
The logic of this custom JS feature. I hope this makes sense.
Crucially, I noticed that there were no origin checks on where this postMessage was coming from. This meant that we could send messages from other origins by using window.open
to open the iframed page in its own tab, and send our custom JS and CSS to it!
As much as I wish it wasn’t sandboxed to hell… it was sandboxed to hell. There was quite a number of very strict limitations on what could be executed by this custom Javascript.
Content Security Policy
First of all, /iframe.html
was in a sandboxed document. What is the sandbox
directive? According to the Mozilla documentation:
It applies restrictions to a page's actions including preventing popups, preventing the execution of plugins and scripts, and enforcing a same-origin policy.
The sandbox attribute provides a way for developers to impose restrictions on what can be executed within the document. The CSP of the document was this:
Content-Security-Policy: sandbox allow-scripts allow-top-navigation-to-custom-protocols allow-top-navigation-by-user-activation allow-downloads
allow-scripts
allows the page to run scripts, but not create pop-up windows. Pop-up windows includealert()
andwindow.open
(on a side note,window.open("https://www.evil.com", "_self")
does not trigger the popup restriction).allow-top-navigation-to-custom-protocols
allows us to navigate to non-HTTP protocols.allow-top-navigation-by-user-activation
allows us to navigate the page using activations such as button clicks - anything that can be considered as an intentional user action.allow-downloads
allows the page to trigger file downloads.
There was also another CSP defined in the HTML via meta
tags.
<meta http-equiv="Content-Security-Policy" content="default-src data: blob:; script-src 'unsafe-inline' 'unsafe-eval' data: blob:; style-src 'unsafe-inline' data: blob:">
default-src
is set todata:
andblob:
, meaning that by default, we cannot load anything from external origins. All such requests to external origins are blocked! This includes images, scripts, and anything else you can think of. This also includesiframe
contents, so we cannot redirect the page withwindow.open
targeted to self, as defined earlier - the iframe will simply reject contents that aren’tblob:
ordata:
.script-src
andstyle-src
are lenient here to allow for customisation. We can execute event handlers and such, as long as it’s all defined within the scope of the document itself.
Finally, X-Frame-Options
was set to sameorigin
, effectively ruling out the prospect of iframing this sandboxed document.
To summarise what we have so far:
We cannot execute
window.open
,alert
, or any other popups.The sandboxed page cannot be iframed, and cannot open iframes.
The sandboxed page cannot connect to any external resources at all.
The sandbox, by nature, is treated as if it is the parent window.
We can execute Javascript and CSS, as long as it’s all defined in the scope of the sandboxed document. We can use
window.open
from our attacker domain to open the sandboxed page, and we can send our payload to it using apostMessage
message. This means that while we can host a phishing page, we need to figure out a way to get the data out.
The Exfiltration
The only way to get data into the sandbox was postMessage. So, naturally, this was probably one of the only ways to get data out of the sandbox. The question is, how do we get a window reference? postMessage is especially dangerous because it’s not limited by factors such as CSP. As I was reviewing the existing functionality within the sandboxed, one line caught my eye.
var source = event.source;
This was it! It turns out that messageEvent
objects, such as postMessage events, have a source
attribute. You can read more about messageEvent
objects here. This attribute can either be a WindowProxy, MessagePort or ServiceWorker. If we read the glossary for the WindowProxy:
All operations performed on a WindowProxy object will also be applied to the underlying Window object it currently wraps. Therefore, interacting with a WindowProxy object is almost identical to directly interacting with a Window object.
With this, we can now send messages back to the attacker window, completely bypassing the CSP! However, to do this, we need to lay some groundwork.
First, we use
window.open
to open the postMessage communication. On our attacker domain, we also set up an event listener to listen to incoming postMessage events.We use our malicious Javascript payload to set up our own event listener in the iframed page. We can’t set
event.source
immediately in the first payload, since we need to smuggle our Javascript into the page first.We send another message to the window - this one has bogus data. Our malicious Javascript uses this message to store
event.source
in a variable, which is aWindowProxy
object that effectively works like aWindow
- they are more or less the same.When the victim enters their credentials into our super cool phishing POC using inline CSS (yes, I spent ages crafting a really convincing CSS for this), the window reference is used to send the credentials back to our attacker domain. Our event listener receives the credentials from the
postMessage
message.
Sequence diagram of the attack
Summary
As fun as this was… it was triaged and paid as Low. This was a fair decision from the team, I have no issue with this. It was an interesting POC to build - and yet again demonstrates that custom JS/CSS functionality is really hard to implement perfectly. I’m sure that there are more ways to exfiltrate data in situations like this - if you have ideas, let me know!
And thank you to Doomerhunter for listening to my stupid ideas. Sometimes the stupid ideas do work. I swear.