When we
wrapped up our recent Pwnium event, we praised the creativity of the submissions and resolved to provide write-ups on how the two exploits worked. We already covered
Pinkie Pie’s submission in a recent post, and this post will summarize the other winning Pwnium submission: an amazing multi-step exploit from frequent Chromium Security Reward winner Sergey Glazunov.
From the start, one thing that impressed us about this exploit was that it involved no memory corruption at all. It was based on a so-called “Universal Cross-Site Scripting” (or UXSS) bug. The UXSS bug in question (
117226) was complicated and actually involved two distinct bugs: a state corruption and an inappropriate firing of events. Individually there was a possible use-after-free condition, but the exploit -- perhaps because of various memory corruption mitigations present in Chromium -- took the route of combining the two bugs to form a “High” severity UXSS bug. However, a
Pwnium prize requires demonstrating something “Critical”: a persistent attack against the local user’s account. A UXSS bug alone cannot achieve that.
So how was this UXSS bug abused more creatively? To understand Sergey’s exploit, it’s important to know that Chromium implements some of its built-in functions using special HTML pages (called WebUI), hosted at origins such as
chrome://about. These pages have access to privileged JavaScript APIs. Of course, a normal web page or web renderer process cannot just iframe or open a
chrome:// URL due to strict separation between
http[s]:// and
chrome:// URLs. However, Sergey discovered that iframing an invalid
chrome-extension:// resource would internally host an error page in the
chrome://chromewebdata origin (
117230). Furthermore, this error page was one of the few internal pages that did not have a
Content Security Policy (CSP) applied. A CSP would have blocked the UXSS bug in this context.
At this point, multiple distinct issues had been abused, to gain JavaScript execution in the
chrome://chromewebdata origin.
The exploit still had a long way to go, though, as there are plenty of additional barriers:
- chrome://chromewebdata does not have any sensitive APIs associated with it.
- chrome://a is not same-origin with chrome://b.
- chrome://* origins only have privileges when the backing process is tagged as privileged by the browser process, and this tagging only happens as a result of a top-level navigation to a chrome:// URL.
- The sensitive chrome://* pages generally have CSPs applied that prevent the UXSS bug in question.
The exploit became extremely creative at this point. To get around the defenses, the compromised
chrome://chromewebdata origin opened a window to
chrome://net-internals, which had an iframe in its structure. Another WebKit bug -- the ability to replace a cross-origin iframe (
117583) -- was used to run script that navigated the popped-up window, simply “back” to
chrome://net-internals (
117417). This caused the browser to reassess the
chrome://net-internals URL as a top-level navigation -- granting limited WebUI permissions to the backing process as a side-effect (
117418).
The exploit was still far from done. It was now running JavaScript inside an iframe inside a process with limited WebUI permissions. It then popped up an about:blank window and abused another bug (
118467) -- this time in the JavaScript bindings -- to confuse the top-level
chrome://net-internals page into believing that the new blank window was a direct child. The blank window could then navigate its new “parent” without losing privileges (
113496). The first navigation was to
chrome://downloads, which gained access to additional privileged APIs. You probably get a sense of where the exploit was headed now. It finished off by abusing privileged JavaScript APIs to download an attack DLL. The same APIs were used to cleverly “download” and run
wordpad.exe from the local disk (thus avoiding the system-level prompt for executing downloads from the internet zone). A design quirk of the Windows operating system caused the attack DLL to be loaded into the trusted executable.
As you can imagine, it took us some time to dissect all of this. Distilling the details into a blog post was a further challenge; we’ve glossed over the use of the UXSS bug to bypass pop-up window restrictions. The UXSS bug was actually used three separate times in the exploit. We also omitted details of various other lockdowns we applied in response to the exploit chain.
What’s clear is that Sergey certainly earned his $60k Pwnium reward. He chained together a whopping 14[*] bugs, quirks and missed hardening opportunities. Looking beyond the monetary prize, Sergey has helped make Chromium significantly safer. Besides fixing the array of bugs, we’ve landed hardening measures that will make it much tougher to abuse
chrome:// WebUI pages in the future.
Posted by Ken Buchanan, Chris Evans, Charlie Reis and Tom Sepez, Software Engineers
[*]14, or thereabouts. It’s easy to lose count.