After the “save” task is put on the queue, it is handled in the chrome process, in the “taskSave” function:
Now that we have a complete picture of what we want to do, let’s begin.
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:
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
This object has its id property set to the arbitrary string “foo”, but 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 “b”, then tab.setAttribute will ultimately be invoked with parameters (“a”, “b”).
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 text-indent: 500px.
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.
Zero Day Initiative – Blog