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

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:
KaraZajac
2026-07-23 21:26:13 -04:00
parent 3edad37184
commit 6edf78f765
3 changed files with 379 additions and 157 deletions
+24 -3
View File
@@ -107,11 +107,32 @@ CRA_DIR := modules/cgroup_release_agent_cve_2022_0492
CRA_SRCS := $(CRA_DIR)/skeletonkey_modules.c
CRA_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CRA_SRCS))
# Family: overlayfs_setuid (CVE-2023-0386) — joins overlayfs family
# Family: overlayfs_setuid (CVE-2023-0386) — joins overlayfs family.
# The exploit needs a FUSE filesystem to export a setuid-root lower layer;
# autodetected via `pkg-config fuse3` (or fuse2). When absent, the module
# compiles as a stub that returns PRECOND_FAIL with a hint to install the
# libfuse3-dev (or libfuse-dev) package and rebuild.
OSU_DIR := modules/overlayfs_setuid_cve_2023_0386
OSU_SRCS := $(OSU_DIR)/skeletonkey_modules.c
OSU_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OSU_SRCS))
# Prefer fuse2 — the public CVE-2023-0386 PoC uses it, and overlay copy-up's
# splice path works cleanly through libfuse2's read_buf; libfuse3's read_buf
# path returns ENOSYS at copy-up on the kernels tested. Fall back to fuse3.
OSU_FUSE2_OK := $(shell pkg-config --exists fuse 2>/dev/null && echo 1 || echo 0)
OSU_FUSE3_OK := $(shell pkg-config --exists fuse3 2>/dev/null && echo 1 || echo 0)
ifeq ($(OSU_FUSE2_OK),1)
OSU_CFLAGS := $(shell pkg-config --cflags fuse) -DOVLSU_HAVE_FUSE
OSU_LIBS := $(shell pkg-config --libs fuse)
else ifeq ($(OSU_FUSE3_OK),1)
OSU_CFLAGS := $(shell pkg-config --cflags fuse3) -DOVLSU_HAVE_FUSE -DOVLSU_FUSE3
OSU_LIBS := $(shell pkg-config --libs fuse3)
else
OSU_CFLAGS :=
OSU_LIBS :=
endif
$(OSU_OBJS): CFLAGS += $(OSU_CFLAGS)
# Family: nft_set_uaf (CVE-2023-32233)
NSU_DIR := modules/nft_set_uaf_cve_2023_32233
NSU_SRCS := $(NSU_DIR)/skeletonkey_modules.c
@@ -299,10 +320,10 @@ TEST_KR_ALL_OBJS := $(TEST_KR_OBJS) $(CORE_OBJS)
all: $(BIN)
$(BIN): $(ALL_OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS) $(OSU_LIBS)
$(TEST_BIN): $(TEST_ALL_OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS) $(OSU_LIBS)
$(TEST_KR_BIN): $(TEST_KR_ALL_OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^