Bug 982906 (CVE-2014-1510)

privilege content loading (ZDI-CAN-2214)

VERIFIED FIXED in Firefox 28

Status

()

defect
VERIFIED FIXED
5 years ago
2 months ago

People

(Reporter: curtisk, Assigned: mrbkap)

Tracking

({sec-critical})

unspecified
mozilla30
Points:
---

Firefox Tracking Flags

(blocking-b2g:1.3+, firefox27 wontfix, firefox28+ verified, firefox29+ verified, firefox30+ verified, firefox-esr2428+ verified, b2g18 fixed, b2g-v1.1hd fixed, b2g-v1.2 fixed, b2g-v1.3 fixed, b2g-v1.3T fixed, b2g-v1.4 fixed, fennec28+)

Details

(Whiteboard: [pwn2own 2014][adv-main28+][adv-esr24.4+])

Attachments

(3 attachments, 1 obsolete attachment)

-----
TL;DR
-----

It is possible for untrusted web content to load a chrome-privileged page by getting JS-implemented WebIDL to call window.open(). A system-principal page loaded in an iframe can then be exploited to bypass the pop-up blocker by invoking document.open() from the unloaded window. This allows web content to open chrome://browser/content/browser.xul in a chrome window with the opener reference pointing to a content iframe. As a result, a user-controlled javascript URI can be loaded with the system principal, which allows arbitrary code execution.


-----------------------------
1. PRIVILEGED CONTENT LOADING
-----------------------------

Chrome pages in Firefox load with the system security principal, which provides access to all browser functionalities, including launching of arbitrary local files with command line arguments. The browser is expected to prevent websites from loading privileged pages, otherwise a UXSS bug may lead to arbitrary code execution.

When window.open() is called, its C++ implementation -- nsGlobalWindow::OpenInternal -- delegates the security check to nsGlobalWindow::SecurityCheckURL:

>>>>>>>>> nsGlobalWindow.cpp >>>>>>>>>
nsGlobalWindow::SecurityCheckURL(const char *aURL)
{
  JSContext       *cxUsed;
  bool             freePass;
  nsCOMPtr<nsIURI> uri;

  if (NS_FAILED(BuildURIfromBase(aURL, getter_AddRefs(uri), &freePass, &cxUsed)))
    return NS_ERROR_FAILURE;

  AutoPushJSContext cx(cxUsed);

  if (!freePass && NS_FAILED(nsContentUtils::GetSecurityManager()->
        CheckLoadURIFromScript(cx, uri)))
    return NS_ERROR_FAILURE;

  return NS_OK;
}
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

The noteworthy thing here is that the loaded URI isn't checked when the freePass flag is set to true. The reference to it is passed to nsGlobalWindow::BuildURIfromBase, which does the following:

>>>>>>>>> nsGlobalWindow.cpp >>>>>>>>>
nsGlobalWindow::BuildURIfromBase(const char *aURL, nsIURI **aBuiltURI,
                                 bool *aFreeSecurityPass,
                                 JSContext **aCXused)
{
  nsIScriptContext *scx = GetContextInternal();
  JSContext *cx = nullptr;

  *aBuiltURI = nullptr;
  *aFreeSecurityPass = false;
  if (aCXused)
    *aCXused = nullptr;

  // get JSContext
  NS_ASSERTION(scx, "opening window missing its context");
  NS_ASSERTION(mDoc, "opening window missing its document");
  if (!scx || !mDoc)
    return NS_ERROR_FAILURE;

  nsCOMPtr<nsIDOMChromeWindow> chrome_win = do_QueryObject(this);

  if (nsContentUtils::IsCallerChrome() && !chrome_win) {
    // If open() is called from chrome on a non-chrome window, we'll
    // use the context from the window on which open() is being called
    // to prevent giving chrome priveleges to new windows opened in
    // such a way. This also makes us get the appropriate base URI for
    // the below URI resolution code.

    cx = scx->GetNativeContext();
  } else {
    // get the JSContext from the call stack
    cx = nsContentUtils::GetCurrentJSContext();
  }
(...)
  nsCOMPtr<nsPIDOMWindow> sourceWindow;

  if (cx) {
    nsIScriptContext *scriptcx = nsJSUtils::GetDynamicScriptContext(cx);
    if (scriptcx)
      sourceWindow = do_QueryInterface(scriptcx->GetGlobalObject());
  }

  if (!sourceWindow) {
    sourceWindow = this;
    *aFreeSecurityPass = true;
  }
(...)
  if (aCXused)
    *aCXused = cx;
  return NS_NewURI(aBuiltURI, nsDependentCString(aURL), charset.get(), baseURI);
}
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

The free pass is granted when it's impossible to determine the window associated with the script context of the call. In theory, this can happen in 2 cases: when there's no native script context (|cx|) on the stack, or it has no related dynamic script context (|scriptcx|). It's the latter case which is interesting.

Whether or not a native script context (JSContext class) links to a dynamic script context depends on how the JSContext was created and is indicated by the |privateIsNSISupports| option in the related ContextOptions object. For instance, contexts created as part of docshell initialization (in the nsJSContext constructor) are always set up with a dynamic script context. This is not the case with contexts created in other places, for example via XPCJSContextStack::GetSafeJSContext(). If one of those ends up calling content-side window.open with the provided arguments, the URI security check can be bypassed.

One way to achieve that is to use JS-implemented WebIDL. It's windowless -- instead, a BackstagePass object is used as a shim for the script global object. Whenever it's executed, the initial call setup pushes a JSContext from XPCJSContextStack::GetSafeJSContext(), because BackstagePass doesn't implement the nsIScriptGlobalObject interface necessary to derive a JSContext from it.

The exploit uses WebRTC peer connection callbacks as a short and simple method of getting JS-implemented WebIDL to call a user-supplied function:

(new mozRTCPeerConnection).createOffer(Map, open.bind(window, "chrome://browser/content", "i"));

If createOffer is called on a connection without any attached MediaStreams, the error callback, passed in as the second argument, is later invoked from PeerConnection.js:

>>>>>>>>>> PeerConnection.js >>>>>>>>>>
  _createOffer: function(onSuccess, onError, constraints) {

    this._onCreateOfferSuccess = onSuccess;

    this._onCreateOfferFailure = onError;                  <<< the bound window.open...
    this._getPC().createOffer(constraints);
  },
(...)

  onCreateOfferError: function(code, message) {
    this.callCB(this._dompc._onCreateOfferFailure, new RTCError(code, message));
    this._dompc._executeNext();
  },
(...)

  callCB: function(callback, arg) {
    if (callback) {
      try {
        callback(arg);                                     <<< ... is invoked here
      } catch(e) {
(...)
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

This causes nsGlobalWindow::OpenInternal to be called while the current JSContext has no dynamic script context. As a result, the free security pass is erroneously granted, and a system-principal page -- chrome://browser/content/browser.xul -- is opened in the iframe named "i".

A SIDE NOTE:
In theory, nsGlobalWindow::BuildURIfromBase could choose to use a different JSContext -- the one associated with the window open() is being called on:

>>>>>>>>> nsGlobalWindow.cpp >>>>>>>>>
  if (nsContentUtils::IsCallerChrome() && !chrome_win) {
    cx = scx->GetNativeContext();                          <<< derived from the window
  } else {
    cx = nsContentUtils::GetCurrentJSContext();            <<< the context of JS-implemented WebIDL
  }
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

However, even though JS-implemented WebIDL code is chrome-privileged, nsContentUtils::IsCallerChrome() returns false. This is caused by the fact that during the call to mozRTCPeerConnection.createOffer, the reference to the bound window.open function is wrapped in a cross-compartment wrapper. As soon as the callback is called, the content compartment of the wrapped function is entered, causing subsequent nsContentUtils::IsCallerChrome() calls to return false.
Component: Security → DOM
Component: DOM → Security
Keywords: sec-critical
Component: Security → DOM
Keywords: sec-critical
The poc in 1.html seems to work on all current browser versions including ESR24.
Alias: ZDI-CAN-2214 → CVE-2014-1510
Summary: privilege content loading → privilege content loading (ZDI-CAN-2214)
Whiteboard: [pwn2own 2014]
Posted patch Most obvious patch (obsolete) — Splinter Review
Please see bug 982909 comment 4.
Attachment #8390165 - Flags: feedback?(bobbyholley)

Comment 6

5 years ago
FWIW, I think this is the commit Blake was talking about: <https://github.com/mozilla/gecko-dev/commit/1df5ae45d>.  But see this: <https://github.com/mozilla/gecko-dev/commit/1df5ae45d#diff-919891bbbc5cc6887ed06d45e6333d47L5513>  This logic was there before that commit.  Going further back in the history, you'll get to <https://github.com/mozilla/gecko-dev/commit/cc6234cba#diff-919891bbbc5cc6887ed06d45e6333d47R3741> which was added in bug 59748.  It's not clear from the bug why this was ever done, but reading it quickly it seems like window.open() did not work from chrome documents back then and maybe that was because we wouldn't get no JS context on the stack or something?  But clearly, none of this stuff applies any more.
Digging through the mentioned aviary branch in bonsai I found these commits:

http://bonsai.mozilla.org/cvsquery.cgi?treeid=default&module=all&branch=AVIARY_1_0_20040515_BRANCH&branchtype=match&dir=&file=&filetype=match&who=&whotype=match&sortby=Date&hours=2&date=explicit&mindate=2004-10-03+09%3A16%3A00&maxdate=2004-10-03+09%3A20%3A00&cvsroot=%2Fcvsroot

which reference bug 172962, which does not contain the patch that actually landed there. Mozilla's checkin policies were much looser in those days, and it shows...
So this looks like it would affect b2g, if the payload were modified. Not sure if uri handling differences provide any mitigation (you can't type chrome:// in the fxos browser, but that is just an gaia app specific detail afaik). I'm testing at the moment, but I would err on the side of affected until proven otherwise.
FWIW, I'm sure the patch is fine. I'm just going to spend some time trying to grok the rest of the setup here to see if anything else interesting jumps out at me.

Also, lol @ "aFreeSecurityPass".
QA Contact: mwobensmith
Ugh.  I hadn't realized we even had that code.  :(

The commit in comment 7 changed the behavior from skipping the check if !cx to skipping it if we can't get a window from the cx, which is not the same thing at all....

It looks like CheckLoadURIFromScript will crash if a null cx is passed in.  We probably want to keep skipping the check if !cxUsed, right?

But also, we should try to just simplify this code.  It's insane.
Comment on attachment 8390165 [details] [diff] [review]
Most obvious patch

I'll file a followup bug for any further investigation.
Attachment #8390165 - Flags: feedback?(bobbyholley) → feedback+
Attachment #8390165 - Flags: review+
Boris points out that the CheckLoadURIFromScript call will segfault if there's a null cx on the stack (which is one of the cases where we previously set |freePass| to true).
So b2g 1.4 definitely is affected by this. Modifying the exploit as follows:

var c = new mozRTCPeerConnection;
c.createOffer(Map,open.bind(top,"chrome://b2g/content/shell.html","i"));

This attempts to load shell.html, which then tries to start a system app. At this point the app or page  crashes since the systemapp content loaded doesnt have the settings permission and is killed. I havent got a working PoC for the rest yet, but this is enough to prove that it needs fixing on b2g.
blocking-b2g: --- → 1.3?
blocking-b2g: 1.3? → 1.3+
IIRC (I can't 100% check since I don't have 1.2 or 1.3 trees currently) PeerConnection is preffed off in 1.2.  It does exist (for audio calls) in 1.3 currently.  For reference, we preffed on PeerConnection in 22, getUserMedia in 20.

However... I'm not certain there aren't other ways to get a window to open and abuse the base-level bug, such as through mozGetUserMedia, and quite possibly other things with success/failure callbacks.  If so, it may be exploitable on 1.2 (or even before if you don't need mozGetUserMedia - if so, it may be exploitable for a Long Time Back).  Note that 1.2 has mozGetUserMedia.

A few attempts to poke at getting this to happen with window.navigator.mozGetUserMedia() failed, but I wouldn't say that means it can't be done.
If it *does* *require* PeerConnection, 1.2 and b2g18 are safe.  B2G18 is safe *if* mozGetUserMedia can provoke it as well but *nothing* else can.
I suspect this can be triggered with any JS implemented API, which PeerConnection was the first to use WebIDL, but WebIDL is not the key here, I'd expect any JS implemented XPCOM component that's exposed to untrusted script would do, and we have a fair number of those.
FWIW, mrbkap's try push with his patch is all green (except for two random oranges).

https://tbpl.mozilla.org/?tree=Try&rev=2c6835a4bca7
jst: So what would be likely venues to unlock this on 1.2 (no PeerConnection) and b2g18?  I know PeerConnection was an early user of JS implementation.  That also explains why getUserMedia seems to be safe; it's not implemented in JS.  PeerConnection should expose 22 and later, probably - any JS APIs before that?
Flags: needinfo?(jst)
Assignee: nobody → mrbkap
Reporter: Mariusz Mlynski
Flags: needinfo?(jst)
Posted patch Patch v2Splinter Review
With the null check.
Attachment #8390165 - Attachment is obsolete: true
Attachment #8390662 - Flags: review?(bzbarsky)
Comment on attachment 8390662 [details] [diff] [review]
Patch v2

r=me
Attachment #8390662 - Flags: review?(bzbarsky) → review+
Comment on attachment 8390662 [details] [diff] [review]
Patch v2

This applies to aurora and beta but not esr24.
Attachment #8390662 - Flags: approval-mozilla-beta?
Attachment #8390662 - Flags: approval-mozilla-aurora?
Comment on attachment 8390662 [details] [diff] [review]
Patch v2

[Triage Comment]
Approving for m-r too since that's where our 28 RC will get built from.  What are the options here for ESR24?  Is it the same risk on that branch?
Attachment #8390662 - Flags: approval-mozilla-release+
Attachment #8390662 - Flags: approval-mozilla-beta?
Attachment #8390662 - Flags: approval-mozilla-beta+
Attachment #8390662 - Flags: approval-mozilla-aurora?
Attachment #8390662 - Flags: approval-mozilla-aurora+
Flags: needinfo?(mrbkap)
Keywords: checkin-needed
Whiteboard: [pwn2own 2014] → [pwn2own 2014][adv-main28+]
Note, I'm getting link errors in webrtc code trying to compile this.
Attachment #8390826 - Flags: review?(jst) → review+
Attachment #8390802 - Flags: approval-mozilla-esr24?
Flags: needinfo?(mrbkap)
Attachment #8390826 - Flags: approval-mozilla-b2g18?
Attachment #8390826 - Flags: approval-mozilla-b2g18?
https://hg.mozilla.org/mozilla-central/rev/5461dd604fcf
Status: NEW → RESOLVED
Last Resolved: 5 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla30

Comment 30

5 years ago
Verified as fixed with the 03/14 Aurora and Nightly on Windows 8.1 64bit, Mac OS X 10.8.5 and Ubuntu 13.04 32bit. No content is loaded anymore for me, I just get errors like these in the Browser console:

Security Error: Content at https://bug982906.bugzilla.mozilla.org/attachment.cgi?id=8390113&t=hGxWhacfjh may not load or link to chrome://browser/content/browser.xul.

Access to 'chrome://browser/content/browser.xul' from script denied
tracking-fennec: --- → ?
Comment on attachment 8390802 [details] [diff] [review]
Patch for esr24

post-landing approval for esr24 - this is already on branch.
Attachment #8390802 - Flags: approval-mozilla-esr24? → approval-mozilla-esr24+
Confirmed bug on Fx27.0.1, Windows 8.
Verified fix on Fx28, Fx24.4.0esr.
Status: RESOLVED → VERIFIED
Keywords: verifyme
Whiteboard: [pwn2own 2014][adv-main28+] → [pwn2own 2014][adv-main28+][adv-esr24.4+]
tracking-fennec: ? → 28+
Duplicate of this bug: 991834
Cody - we should avoid talking about separate security issues in the same bug, since we don't want to accidentally make unrelated things public when we open up a fixed bug.

Comment 36

5 years ago
I'm always bad about that.  I'll start hopping on irc or something with some of you guys to discuss these things instead.  Nice catch as always, I just get too excited and allow myself to get carried away when it comes to these things sometimes.
Group: core-security
Component: DOM → DOM: Core & HTML
Product: Core → Core
You need to log in before you can comment on or make changes to this bug.