Files
CHARON/README.md
T
leviathan 95b37066df v1.1.0 — auto-discovery + --list-baits + ANSI Shadow banner
* New --auto flag: walk /usr/bin, /usr/sbin, /usr/local/{bin,sbin},
  /usr/lib/openssh, /usr/libexec, /bin, /sbin; try every SUID/SGID
  regular file as a bait against the requested target. Skips
  known-interactive baits (su, sudo, newgrp, pkexec, mount, …).
  Per-bait budget capped (5 rounds × 2000 inner) + 60s wall clock,
  so a full scan finishes in ~10s even on systems where no bait
  opens the requested file.

* New --list-baits flag: enumerate built-in + discoverable bait
  candidates without firing the exploit. Useful for distro surveys.

* SIGKILL daemonic baits (ssh-agent etc.) instead of waiting
  forever in waitpid().

* Accept SGID-shadow baits, not just SUID-root — chage on
  Debian/Ubuntu is mode 2755 not 4755 and we kept skipping it.

* Banner upgraded to ANSI Shadow block letters with a Styx wave
  motif beneath the version line.

* Cleaner diagnostics: distinguish "primitive fires but no bait
  opens this file" from "kernel patched (no pidfd_getfd success)".

Tested on Debian 13 / kernel 6.12.88-kctf-poc:
- default run hits /etc/shadow in 1 fork (~160 tries, <1s)
- --auto on /etc/sudoers correctly times out in 11s with diagnostic
- --quiet pipes 35 lines of pure shadow to stdout
- --verbose shows per-hit + final stats
- --list-baits enumerates 26 candidates incl. /usr/bin/chage
2026-05-15 23:33:59 -04:00

6.6 KiB
Raw Blame History

  ____  _   _    _    ____   ___  _   _
 / ___|| | | |  / \  |  _ \ / _ \| \ | |
| |    | |_| | / _ \ | |_) | | | |  \| |
| |___ |  _  |/ ___ \|  _ <| |_| | |\  |
 \____||_| |_/_/   \_\_| \_\\___/|_| \_|

  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 (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

# 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

./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 bait)
./charon -a                  # auto-discover SUID/SGID baits if built-ins miss
./charon -L                  # list candidate baits without trying any
./charon --help

Auto-discovery

--auto walks /usr/bin, /usr/sbin, /usr/local/{bin,sbin}, /usr/lib/openssh, /usr/libexec, /bin, /sbin, finds every SUID/SGID regular file (excluding interactive baits like su, sudo, newgrp, pkexec), and tries each as a bait against the requested target. Per-bait budget is tight (5 rounds × 2000 inner) so a full scan finishes in ~10 seconds even when nothing matches.

--list-baits is the read-only version — it enumerates the same candidates without firing the exploit. Useful for surveying which distros ship which baits.

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 <user>) /etc/shadow Most Debian, Ubuntu, Fedora
/usr/sbin/chage /etc/shadow RHEL / Rocky / Alma family
/usr/bin/passwd (passwd -S <user>) /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.
  • 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()           ║
              ╚══════════════════════════════╝