overlayfs_setuid: working CVE-2023-0386 exploit (libfuse) — lands real root
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 shipped module used a bogus chown-the-merged-view technique that never
worked. Rewrote it as a faithful port of the public PoC (xkaneiki): a libfuse
filesystem exports a setuid-root /file (st_uid=0, mode 04777), mounted in the
init ns via the setuid fusermount helper; then unshare(USER|NS) + overlay with
that FUSE mount as lowerdir; open(merged/file, O_WRONLY) triggers copy-up which
materialises upper/file as a genuine setuid-root binary; the unprivileged parent
execs it for real root.
VM-verified landing real root on Ubuntu 22.04.0 / 5.15.0-25 (uid=0 witnessed
out-of-band: /tmp/skeletonkey-ovlsu-pwned shows uid=0(root), and the dropped
setuid /tmp/.suid_bash runs with euid=0).
Four things were each required and took isolation to find:
1. Overlay refuses a userns-mounted FUSE lowerdir (ENOSYS) -> FUSE must be
mounted in the init ns via fusermount (libfuse). A raw /dev/fuse server was
tried and abandoned (fragile; destabilised the kernel on malformed INIT).
2. fuse2 low-level API (fuse_mount/fuse_new/fuse_loop_mt, empty args); fuse_main
advertises copy_file_range caps that ENOSYS at copy-up with no fallback.
3. read_buf callback (copy-up splice read path).
4. ioctl callback (copy-up's FS_IOC_GETFLAGS) — the last missing piece.
libfuse is linked conditionally via pkg-config (fuse/fuse3), matching the
pack2theroot+libglib precedent; without it the module stubs to PRECOND_FAIL.
Makefile detects it and adds $(OSU_LIBS) to the link. Module header + opsec
notes rewritten to the real technique; docs/EXPLOITED.md updated. 148-test unit
harness green.
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:
@@ -24,6 +24,7 @@ tamper check).
|
||||
|---|---|---|---|
|
||||
| `refluxfs` | CVE-2026-64600 | Rocky 9.8 / 5.14.0-687.el9 | full chain, `/etc/passwd` → root (earlier) |
|
||||
| `overlayfs` | CVE-2021-3493 | Ubuntu 20.04.0 / 5.4.0-26 | userns + xattr copy-up, clean |
|
||||
| `overlayfs_setuid` | CVE-2023-0386 | Ubuntu 22.04.0 / 5.15.0-25 | **after a full rewrite** — see below |
|
||||
| `pwnkit` | CVE-2021-4034 | Ubuntu 20.04.0 / polkit 0.105-26ubuntu1 | **after a fix** — see below |
|
||||
| `sudo_runas_neg1` | CVE-2019-14287 | Ubuntu 18.04.2 / sudo 1.8.21p2 + sudoers `(ALL,!root)` | `sudo -u#-1` → uid 0 |
|
||||
|
||||
@@ -39,6 +40,27 @@ tamper check).
|
||||
(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`)
|
||||
- **`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
|
||||
real bug needs a **FUSE lower layer** exporting a setuid-root file; overlay
|
||||
copy-up then materialises it in the real upper as a genuine setuid-root
|
||||
binary. Key findings from the port (all four were required):
|
||||
1. Overlay refuses a **userns-mounted** FUSE lowerdir (ENOSYS) — FUSE must
|
||||
be mounted in the **init ns** via the setuid `fusermount` helper (libfuse
|
||||
does this). A raw `/dev/fuse` server was tried and abandoned: its INIT
|
||||
handshake needs `poll()` on the non-blocking fd, and a malformed reply
|
||||
destabilised the kernel — fragile and inappropriate. libfuse is linked
|
||||
conditionally (pkg-config `fuse`/`fuse3`), matching `pack2theroot`.
|
||||
2. **fuse2** low-level API (`fuse_mount`/`fuse_new`/`fuse_loop_mt`, empty
|
||||
args) — `fuse_main` advertises splice/copy_file_range caps that make the
|
||||
kernel attempt `copy_file_range` at copy-up → ENOSYS with no fallback.
|
||||
3. **`read_buf`** callback (copy-up's splice read path).
|
||||
4. **`ioctl`** callback — copy-up issues `FS_IOC_GETFLAGS` on the lower; a
|
||||
server without an ioctl handler returns ENOSYS and copy-up fails. This
|
||||
was the last missing piece.
|
||||
Debugging was isolated by driving the exploit orchestration against the public
|
||||
PoC's `./fuse`, then swapping servers, then comparing `fops`.
|
||||
- **`cgroup_release_agent`** — two real bugs fixed (commit `8c45b2b`): it read
|
||||
`getuid()` **after** `unshare(CLONE_NEWUSER)` (→ `65534`, so `uid_map` write
|
||||
was `"0 65534 1"` → EPERM), and it omitted `CLONE_NEWCGROUP` (→ cgroup-v1
|
||||
|
||||
Reference in New Issue
Block a user