Share
## https://sploitus.com/exploit?id=B95F550A-81CB-538B-9F56-C93BD84E831B
```
  ____  _   _    _    ____   ___  _   _
 / ___|| | | |  / \  |  _ \ / _ \| \ | |
| |    | |_| | / _ \ | |_) | | | |  \| |
| |___ |  _  |/ ___ \|  _  *"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 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 `) | `/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()           โ•‘
              โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
```