Closed
Bug 982906
(CVE-2014-1510)
Opened 11 years ago
Closed 11 years ago
privilege content loading (ZDI-CAN-2214)
Categories
(Core :: DOM: Core & HTML, defect)
Core
DOM: Core & HTML
Tracking
()
People
(Reporter: curtisk, Assigned: mrbkap)
References
Details
(Keywords: sec-critical, Whiteboard: [pwn2own 2014][adv-main28+][adv-esr24.4+])
Attachments
(3 files, 1 obsolete file)
3.77 KB,
patch
|
bzbarsky
:
review+
lsblakk
:
approval-mozilla-aurora+
lsblakk
:
approval-mozilla-beta+
lsblakk
:
approval-mozilla-release+
|
Details | Diff | Splinter Review |
4.00 KB,
patch
|
lsblakk
:
approval-mozilla-esr24+
|
Details | Diff | Splinter Review |
4.00 KB,
patch
|
jst
:
review+
|
Details | Diff | Splinter Review |
-----
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.
Reporter | ||
Updated•11 years ago
|
See Also: → CVE-2014-1511
Reporter | ||
Updated•11 years ago
|
Keywords: sec-critical
Updated•11 years ago
|
Component: Security → DOM
Updated•11 years ago
|
Component: DOM → Security
Keywords: sec-critical
Updated•11 years ago
|
Component: Security → DOM
Keywords: sec-critical
Comment 4•11 years ago
|
||
The poc in 1.html seems to work on all current browser versions including ESR24.
status-firefox27:
--- → affected
status-firefox28:
--- → affected
status-firefox29:
--- → affected
status-firefox30:
--- → affected
status-firefox-esr24:
--- → affected
Updated•11 years ago
|
Alias: ZDI-CAN-2214 → CVE-2014-1510
Summary: privilege content loading → privilege content loading (ZDI-CAN-2214)
Updated•11 years ago
|
Whiteboard: [pwn2own 2014]
Assignee | ||
Comment 5•11 years ago
|
||
Please see bug 982909 comment 4.
Attachment #8390165 -
Flags: feedback?(bobbyholley)
Comment 6•11 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.
Comment 7•11 years ago
|
||
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...
Comment 8•11 years ago
|
||
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.
Comment 9•11 years ago
|
||
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".
Updated•11 years ago
|
QA Contact: mwobensmith
Comment 10•11 years ago
|
||
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 11•11 years ago
|
||
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+
Updated•11 years ago
|
Attachment #8390165 -
Flags: review+
Comment 12•11 years ago
|
||
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).
Comment 13•11 years ago
|
||
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.
status-b2g18:
--- → ?
status-b2g-v1.2:
--- → ?
status-b2g-v1.3:
--- → ?
status-b2g-v1.4:
--- → affected
Updated•11 years ago
|
blocking-b2g: --- → 1.3?
Updated•11 years ago
|
blocking-b2g: 1.3? → 1.3+
Updated•11 years ago
|
Comment 14•11 years ago
|
||
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.
Comment 15•11 years ago
|
||
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.
Comment 16•11 years ago
|
||
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.
Comment 17•11 years ago
|
||
FWIW, mrbkap's try push with his patch is all green (except for two random oranges).
https://tbpl.mozilla.org/?tree=Try&rev=2c6835a4bca7
Comment 18•11 years ago
|
||
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)
Comment 19•11 years ago
|
||
Like jst said, any JS component that's exposed to the web. Some plausible candidates:
http://mxr.mozilla.org/mozilla-b2g18/search?string=JavaScript-navigator-property&find=manifest&findi=&filter=^[^\0]*%24&hitlimit=&tree=mozilla-b2g18
http://mxr.mozilla.org/mozilla-b2g18/search?string=JavaScript-global-property&find=manifest&findi=&filter=^[^\0]*%24&hitlimit=&tree=mozilla-b2g18
http://mxr.mozilla.org/mozilla-central/search?string=nsIClassInfo.DOM_OBJECT&find=&findi=&filter=^[^\0]*%24&hitlimit=&tree=mozilla-central
(that last list has some overlap with the previous two).
Updated•11 years ago
|
Updated•11 years ago
|
Assignee: nobody → mrbkap
Comment 20•11 years ago
|
||
Reporter: Mariusz Mlynski
Updated•11 years ago
|
Flags: needinfo?(jst)
Assignee | ||
Comment 21•11 years ago
|
||
With the null check.
Attachment #8390165 -
Attachment is obsolete: true
Attachment #8390662 -
Flags: review?(bzbarsky)
Comment 22•11 years ago
|
||
Comment on attachment 8390662 [details] [diff] [review]
Patch v2
r=me
Attachment #8390662 -
Flags: review?(bzbarsky) → review+
Assignee | ||
Comment 23•11 years ago
|
||
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 24•11 years ago
|
||
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+
Updated•11 years ago
|
Flags: needinfo?(mrbkap)
Updated•11 years ago
|
Keywords: checkin-needed
Whiteboard: [pwn2own 2014] → [pwn2own 2014][adv-main28+]
Comment 25•11 years ago
|
||
Keywords: checkin-needed
Assignee | ||
Comment 26•11 years ago
|
||
Note, I'm getting link errors in webrtc code trying to compile this.
Updated•11 years ago
|
tracking-firefox-esr24:
--- → 28+
Assignee | ||
Comment 27•11 years ago
|
||
Attachment #8390826 -
Flags: review?(jst)
Updated•11 years ago
|
Attachment #8390826 -
Flags: review?(jst) → review+
Assignee | ||
Updated•11 years ago
|
Attachment #8390802 -
Flags: approval-mozilla-esr24?
Flags: needinfo?(mrbkap)
Assignee | ||
Updated•11 years ago
|
Attachment #8390826 -
Flags: approval-mozilla-b2g18?
Updated•11 years ago
|
Attachment #8390826 -
Flags: approval-mozilla-b2g18?
Status: NEW → RESOLVED
Closed: 11 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla30
Comment 29•11 years ago
|
||
https://hg.mozilla.org/releases/mozilla-aurora/rev/110cdd90b1ed
https://hg.mozilla.org/releases/mozilla-beta/rev/fdbb3889ca2b
https://hg.mozilla.org/releases/mozilla-release/rev/fdbb3889ca2b
https://hg.mozilla.org/releases/mozilla-b2g28_v1_3/rev/fdbb3889ca2b
https://hg.mozilla.org/releases/mozilla-b2g26_v1_2/rev/1e2d55676dcf
https://hg.mozilla.org/releases/mozilla-esr24/rev/ca653af870e2
https://hg.mozilla.org/releases/mozilla-b2g18/rev/aa48d7c05e47
https://hg.mozilla.org/releases/mozilla-b2g18_v1_1_0_hd/rev/aa48d7c05e47
status-b2g-v1.1hd:
--- → fixed
status-b2g-v1.3T:
--- → fixed
Comment 30•11 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
Reporter | ||
Updated•11 years ago
|
tracking-fennec: --- → ?
Comment 31•11 years ago
|
||
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+
Comment 32•11 years ago
|
||
Confirmed bug on Fx27.0.1, Windows 8.
Verified fix on Fx28, Fx24.4.0esr.
Status: RESOLVED → VERIFIED
Keywords: verifyme
Updated•11 years ago
|
Whiteboard: [pwn2own 2014][adv-main28+] → [pwn2own 2014][adv-main28+][adv-esr24.4+]
Updated•11 years ago
|
tracking-fennec: ? → 28+
Comment 35•11 years ago
|
||
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•11 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.
Updated•10 years ago
|
Group: core-security
Updated•6 years ago
|
Component: DOM → DOM: Core & HTML
You need to log in
before you can comment on or make changes to this bug.
Description
•