Security Research
by Alexander Sotirov
ProFTPD ASCII translation heap overflow exploit
This is an exploit for the ProFTPD heap overflow vulnerability discovered by Mark Dowd. It affects ProFTPD 1.2.7 through 1.2.9rc2. Version 1.2.5 (which ships with Debian stable) is not vulnerable.
Downloads
Screenshot
$ ./proftpd-not-pro-enough -h : proftpd-not-pro-enough : ProFTPD remote exploit for CAN-2003-0831 by Solar Eclipse <solareclipse@phreedom.org> Usage: ./proftpd-not-pro-enough [options] <host> -f <filename> filename to create (random by default) -d <dir> writable directory -u <user> user name (anonymous by default) -p <password> password -v verbose mode If you do not supply any options, an anonymous connection will be established and the exploit will attempt to find a writable directory. Examples: ./proftpd-not-pro-enough -v localhost ./proftpd-not-pro-enough -u user -p secret -d /incoming 192.168.0.1 $ ./proftpd-not-pro-enough -d '.' -u test -p pass -f test localhost : proftpd-not-pro-enough : ProFTPd remote exploit by Solar Eclipse <solareclipse@phreedom.org> : connecting to localhost 220 ProFTPD 1.2.7 Server (ProFTPD Default Installation) [localhost] : loging in as test/pass : using writable directory ., filename is test : exploiting server (offset 0xffff) stage1 shellcode failed : exploiting server (offset 0xfffe) Execution of stage1 shellcode succeeded, sending stage2 bash-2.05b#
Vulnerability details
This vulnerability is triggered by downloading a specially crafted file in ASCII mode. The vulnerability is in data_xfer(), which calls _xlate_ascii_write() with a buffer, the length of the data in the buffer and its size. All '\n' characters in the buffer are expanded by shifting the rest of the data in the buffer and prepending a '\r' character.
By default the buffer is 1535 bytes long and holds 1024 bytes of data. If there are more than 511 new lines in the data, _xlate_ascii_write() allocates a new buffer and copies the data there. The size of the new buffer is stored in session.xfer.bufsize (a global variable!). Since we can create a file with as many new lines in it as we want, we control the value of session.xfer.bufsize.
After _xlate_ascii_write() returns, data_xfer() reads in the next 1024 byte chunk of data and puts it in the same buffer it used the first time. Then it calls _xlate_ascii_write() again, passing it a pointer to the 1535 byte buffer and session.xfer.bufsize (which is controlled by us and could have a value greater then 1535) as its size.
When _xlate_ascii_write() shifts the data in the buffer to make space for a '\r' character, it will shift data past the end of the buffer. This allows us to overwrite the next chunk in memory.
Basic exploitation
The standard approach to a heap overflow is to overwrite the next malloc chunk and use the unlink() macro in free() to overwrite 4 bytes of data at an arbitrary location. Unfortunately this technique is not applicable here, bacause proftpd never calls free(). All memory allocation in proftpd is done by the memory pool code in src/pool.c. When a block is no longer needed it is added to the free pool. All memory allocation requests are satisfied with blocks from the free pool if it is not empty. Only when all memory blocks from the free pool are re-used does the allocation code call malloc().
The first step in exploiting ProFTPD was the development of a memory allocation debugger, which allows us to monitor the internal memory allocation code in ProFTPD. The analyzis of the data reveals that if we start with a fresh FTP process and issue a "TYPE A", "PASV", "RETR filename" command sequence, the memory block after the buffer will contain a struct pool. This structure stores information about a memory pool, including a cleanup function pointer.
Overwriting this function poiner will force a jump to a location of our choice. If we knew where our shellcode is, we could easily exploit the process.
OVERWRITE and SHIFT
This vulnerability is different than the usual buffer overflows because we can do more then just overwrite the next chunk. The _xlate_ascii_write() function can be called multiple times and can be used to perform two operations on the data in the next memory block (referred to as _nextchunk_ from now on).
- OVERWRITE(data, len) overwrites the memory from nextchunk[0] to nextchunk[len] with data.
- SHIFT(count, len) shifts the memory from nextchunk[0] to nextchunk[len] by count bytes to the right.
Using these two operations allows us to overwrite the 2 least significant bytes of a pointer in nextchunk and shift it to pool->cleanup. This pointer normally points at nextchunk+512. By overwriting its 2 least significant bytes with 0xffff we can make the code jump to a location between nextchunk+512 and nextchunk+64K.
To populate this area with shellcode, we'll use 32 chunks of data, each containing one more '\n' than the previous one. This will cause _xlate_ascii_write() to allocate a new 2K buffer for each of them, populating the 64K of memory with NOP-sleds and shellcode.
Notes
This exploit will only run on Linux/x86. The shellcode consists of 2 stages. Stage 1 reuses the existing FTP connection and reads the stage2 shellcode. The stage2 shellcode breaks out of chroot and spawns a root shell.
If the server was compiled with support for Linux capabilities, the shellcode will not be able to break out of chroot, as it will be running without root privileges. In this case you will see:
: exploiting server (offset 0xffff) Execution of stage1 shellcode succeeded, sending stage2 Stage2 shellcode failed.
This means that the exploit worked, but stage2 shellcode failed to break out of chroot or was unable to execute /bin/sh for some reason.
Since the exploitation is heavily dependent on the state of the heap, it might not run on some configurations. When it runs it should be completely distribution independent.
If you run the exploit with no options, it will try 6 most likely offsets. I believe that if these offsets fail the exploit will almost certainly not work at all. However if you are desperate you can try the -b option.
If you specify the -b option, the exploit will try all offsets from 0xffff to 0, in decrements of 1000. It will take it about 120 tries (because it checks odd and even addresses) to get through the whole range. If this fails, there is no hope.
The -o option allows you to specify a single offset to try. Useful for debugging. When used in conjunction with -b, the brute force will start at the specified offset and go down to 0.
One problem you may run into while bruteforcing is the maximum number of connections allowed by the server. When a process crashes, ProFTPD does not update its scoreboard and thinks that the process is still running. It is very easy to exhaust the number of available connections, which leads to a DoS attack. This results in a message like this:
530 Sorry, the maximum number of allowed clients (10) are already connected.
ProFTPD does not handle SIGILL properly. If the process crashes with a SIGILL during bruteforcing it will get in a infinite loop with 100% CPU usage. If you are lucky and the exploit succeeds on the next try, remember to kill the runaway process.