FTP (300 points)

We found an ftp service, I'm sure there's some way to log on to it.

nc 54.175.183.202 12012
ftp_0319deb1c1c033af28613c57da686aa7

We're given a binary. Let's see what it is:

 

$ file ftp_0319deb1c1c033af28613c57da686aa7
ftp: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=43afbcd9f4e163f002970b9e69309ce0f1902324, stripped

 

 

Just a basic 64-bit binary. It is stripped, so we won't be getting help with names of variables and such. Let's run it and see what it does:

 

$ ./ftp 
[+] Creating Socket
[+] Binding
[+] Listening
[+] accept loop

 

Checking netstat, the binary has set up a listener.

 

$ netstat -natup | grep ftp
tcp        0      0 0.0.0.0:12012           0.0.0.0:*               LISTEN      2290/ftp

 

Time to connect and see what it does.

 

$ nc 127.0.0.1 12012
Welcome to FTP server
help
USER PASS PASV PORT
NOOP REIN LIST SYST SIZE
RETR STOR PWD CWD

 

So speed up the write-up, this binary basically behaves like an ftp service. Before you can do anything, you need to log in (it won't let you do anything else). Playing around with the commands a bit, you discover the syntax is "USER myuser" and then "PASS mypass". Now the question is how do we determine the username and password? Here's an example attempt:

 

$ nc 127.0.0.1 12012
Welcome to FTP server
USER myuser
Please send password for user myuser
PASS mypass
Invalid login credentials

 

The words themselves are not helpful, but we can use those strings to help find the correct location in the binary where authentication happens. Tracing those strings back, you discover they exist in a function at 0x40159B. This function appears to be the login function:

 

FTP IDA Login

 

Notice the string "blankwall". Without too much analysis we can go ahead and try that as the username. If we set out breakpoint at 0x401740 (that box below the one with blankwall string), we notice that indeed the username is supposed to be blankwall.

 

Now is the fun part. Just discovering that the username is "blankwall" is pretty easy. The call after that appeared to take the password and change it to some 32-bit integer value -- like a hash. It then compares that value with 0x0D386D209. Therefore, to successfully login, we need a password that, when hashed, will output that value. Let's take a closer look at the hash algorithm:

 

FTP 2

 

At this point, there are a few ways to tackle the problem. Lately I've been trying to understand Symbolic Execution better, as well as utilize the Angr framework (thanks Shellphish!). A hashing function like this is really where Angr should shine. It would also be possible to solve this by reversing the function itself, and maybe creating a cracking program, but let's see how we can do this with angr.

 

 I've commented in-line in my solution:

 

import angr

# Load up the binary
b = angr.Project("ftp")

# Create a state so that we can start the application
# at our password hash function instead of the entry point
initial_state = b.factory.entry_state(addr=0x401540)

# Create the path group to execute through the binary
# Note that we're using the initial_state variable from
# above so that we start at the hash function
pg = b.factory.path_group(initial_state,immutable=False)

# Explore to end of the hash function
# num_find = 16 in this case means that the hash function
# can be run on a password up to 16 chars in length
pg.explore(find=(0x401596),num_find=16)

# Angr is Big Endian in nature. Linux is Little Endian
# This is why the desired output of "0xD386D209" is reversed
wantedOut = 0x09d286d3

# Loop through discovered paths out to find the solution
for path in pg.found[1:]:
        # Copy the state so that we don't mess it up
        s = path.state.copy()

        # Get the location of the output ($rbp - 0x4)
        hashed = s.memory.load(s.regs.rbp - 4,4)

        # Add the hash that we want in the end
        s.add_constraints(hashed == wantedOut)

        try:
                # Grab the password location
                password = s.memory.load(s.memory.load(s.regs.rbp-0x18,4),pg.found.index(path))
        except:
                # Accept defeat for this length of password and move on to the next
                continue

        # The hash is always going to hash the newline at the end as well
        # Remember angr is Big Endian. That's why we're playing the constraint at
        # the beginning of the array. It's actually the end of the string
        s.add_constraints(password[7:0] == ord("\n"))

        # try to find printable
        for i in xrange(8,len(password),8):
                # While we don't technically have to do this, this is basically
                # just telling angr that we want the password to be in the character
                # range of [a-zA-Z0-9]. If you wanted other characters, you can change this
                s.add_constraints(
                                s.se.Or(
                                        s.se.And(
                                                s.se.UGE(password[i+7:i],ord("a")),
                                                s.se.ULE(password[i+7:i],ord("z"))
                                                ),

                                        s.se.And(
                                                s.se.UGE(password[i+7:i],ord("A")),
                                                s.se.ULE(password[i+7:i],ord("Z"))
                                                ),

                                        s.se.And(
                                                s.se.UGE(password[i+7:i],ord("0")),
                                                s.se.ULE(password[i+7:i],ord("9"))
                                                ),

                                ))

        # Ask the constraint solver to find us passwords. I've set this to try to find me up
        # to 5 passwords at a time. You could change that to 1 to be quicker, or higher
        # if you were interested in finding more valid passwords.
        try:
                print(s.se.any_n_str(password,5))
        except:
                pass

 

Then we try running this script:

 

$ python angr_script.py 
[]
['dNpJie\n', 'dNpJjD\n', 'dNpKHe\n', 'dNpKID\n', 'dONkie\n']
['XAMJRoaH\n', 'pDH6Eybh\n', 'pDH6EzAh\n', 'pDH6FXbh\n', 'z5g3AnK7\n']
['6O0wO9p4B\n', 'LeoZfblka\n', 'LeoZfbmJa\n', 'LeoZfcKka\n', 'Zfl0HPt9y\n']
['DP0GBkucDf\n', 'DP0GBkucEE\n', 'DP0GBkvBDf\n', 'DP0GBkvBEE\n', 'V88yZlY9iZ\n']

 

All of those entries should be valid passwords for this user. Let's give d0Nkie a shot:

 

$ nc 127.0.0.1 12012
Welcome to FTP server
USER blankwall
Please send password for user blankwall
PASS dONkie
logged in

 

Yay! At this point I played around with the binary some more as the functionality was unlocked. As is normal with FTP, you can do things like "pasv", which will open a port you can connect to with another binary (such as netcat), then run RETR with a file name to have that file be pushed out the new port. The irritating thing about this is it requires re-connecting every command you want to run.

 

Performing a directory listing, I discovered the following:

 

drwxr-xr-x 1     0     0         4096 Sep 20 05:22 ftp_0319deb1c1c033af28613c57da686aa7
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 .bashrc
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 .bash_history
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 run.sh
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 flag.txt
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 .profile
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 .bash_logout
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 re_solution.txt
drwxr-xr-x 1     0     0         4096 0         4096  .selected_editor

 

It was noted that flag.txt is actually for the FTP2 exploit challenge, not the FTP reversing challenge. The file "re_solution.txt" was actually the flag file for this challenge. Sadly, if you try to do "RETR re_solution.txt", you are greeted with the text "Invalid character specified". There's a little trickery here because they set up their directory structure intentionally irritating.

 

You can trace back that error to the following parts of the code:

 

FTP 3

 

Basically, that loop there will end either when the full path (to include file name) hits a "f" character, or completes iterating the string length. So basically, they've outlawed the "f" character, which makes sense if they wanted to make one challenge require reversing and the other exploitation.

 

But we didn't ask for a file with "f" in it! Well, they were jerks. If you do a pwd you discover they named their current folder "ctf", which nicely contains an "f". Initially I tried to go through proc and use symlinks to get the file, but that didn't end up working. At some point, I decided to review the code again to see if I was missing something. Indeed I was...

 

FTP 4

 

If you look at the left side of the display, it shows that the command you enter is checked for basically similar to the following code:

 

if command == "USER" then ...
elif command == "PASS" then ...
elif command == "HELP" then ...
etc

 

If we follow that to it's end, we discover there's a command that isn't documented in the HELP statement, but is being checked for, called "RDF". If we open up that function, we see the following:

 

FTP 5

 

As you can see, the file re_solution.txt is opened, read, and displayed to the user. We don't even have to use PASV! I probably could have saved myself a little heartache by doing strings first, but I didn't.

 

Welcome to FTP server
USER blankwall
Please send password for user blankwall
PASS dONkie
logged in
RDF
flag{n0_c0ok1e_ju$t_a_f1ag_f0r_you}

 

Flag: flag{n0_c0ok1e_ju$t_a_f1ag_f0r_you}