After the “save” task is put on the queue, it is handled in the chrome process, in the “taskSave” function:
At , both
origin to the string
this.notifications[origin] will not access a normal data property. Instead, it will access the object’s prototype. This prototype is
this.notifications is a plain
Object.prototype with only one restriction: the value we write must have an
id property that matches the property name we are writing to.
Object.prototype. The exploit will use this corruption to gain chrome-level XSS during tab restoration, leading to native code execution outside the sandbox.
Now that we have a complete picture of what we want to do, let’s begin.
As mentioned above, before we can invoke
components. This is a different object than a much more limited object confusingly also named
Components, which is intended to be exposed to untrusted script.
To gain access to
components, the attacker script performs the following steps. Note that all this is made possible because the attacker script has already gained full native code execution within the renderer sandbox, as detailed in part one of this series:
system by setting the corresponding flag in memory.
2 -- Patch
CanCreateWrapper to always return
NS_OK. This prevents further security checks on the calling context.
3 -- Call the
GetComponents method to add the
components object to the scope.
Triggering the Prototype Pollution Primitive
Once we have obtained the
Remember that a limitation applies to the way that we can overwrite properties of
Object.prototype: we can set any property
name to any value
val.id must equal
name. For our purposes, the exact value of
val will not matter. Only its string representation is important (more precisely, the result of running the ECMAScript
This object has its
id property set to the arbitrary string
ToString will represent the object by just the string
"bar". Therefore, as long as we only care about the string representation, we can set any property of
Object.prototype to any value we desire.
Leveraging the Prototype Pollution for Sandbox Escape
Consider the following code in browser/components/sessionstore/TabAttributes.jsm, which executes in the chrome process:
Note that a
for ... in loop will traverse all properties found in the prototype chain, and not only the properties found on the object itself. Therefore, by invoking the code shown above after we have polluted
Object.prototype, we can cause
tab.setAttribute to be called with arbitrary parameters. This will set an arbitrary HTML (technically XUL) attribute of a tab.
How can we cause this function to run? It turns out that the only time it is called is during the restoration of tabs. There are multiple ways to trigger this functionality:
1 -- Session restoration after restarting the browser.
2 -- Use of the “reopen closed tab” feature (Ctrl+Shift+T).
3 -- Reactivating a tab after “Tab Unloading”, which occurs when Firefox starts to run out of memory.
4 -- Automatically restoring a tab after it has crashed.
The first choice is not an option, since restarting the browser would not preserve the polluted prototype. In the real world, waiting for option #2 might work, but it requires user interaction, making it unsuitable for Pwn2Own. It’s also possible to force option #3 by allocating large chunks of memory. However, by default, it takes at least 10 minutes of inactivity before unloading will happen, which exceeds the Pwn2Own time constraint. This leaves just option #4. Fortunately, crashing the renderer process is trivial: we have already achieved memory corruption, and we can simply write to an invalid address to force a segmentation fault.
So far, the sandbox escape exploit proceeds as follows:
1 -- Trigger the prototype pollution, adding a property and value to
Object.prototype in the chrome process. The name/value pair we add corresponds to the parameters we want to pass to
tab.setAttribute. For example, if we add a property named
"a" with string value
tab.setAttribute will ultimately be invoked with parameters
2 -- Open a new background tab. Note that a simple
window.open method call without prior user interaction is blocked by the popup blocker. However, the check is entirely renderer-side, and the
services.ww.openWindow API obtained from the
components object has no such restriction.
3 -- In this background tab, crash the renderer. The chrome process will immediately restore the background tab. The polluted prototype will cause the tab restoration logic to set our chosen attribute on the tab.
Next, we must consider: what parameters do we want to pass to
tab.setAttribute? As the browser UI that contains the tab element is written not in HTML but rather the similar XUL markup language, attributes such as “onload” or “onerror” that are commonly used for XSS do not seem to work. Going through a list of XUL event handlers, there are only two that seem to work without any direct user interaction: “onoverflow” and “onunderflow”. These are triggered when the tab’s title text starts to exceed or no longer exceeds the available space. We can trigger the former by setting a
style attribute with the value
Here is a short video demonstrating running the full exploit against Mozilla Firefox 100.0.1 (64-bit):
Modern browsers process large volumes of data coming from numerous untrusted sources. Modern browser architecture goes a long way towards containing damage in cases where the renderer process is compromised. However, there remain multiple security checks that are performed on the renderer side. We have seen how these checks could be bypassed, ultimately leading to full compromise of the main browser process. In general, it is wise to reduce renderer-side security checks and move them to the main process wherever it is practical.
But You Told Me You Were Safe: Attacking the Mozilla Firefox Sandbox (Part 2)