Files
SKELETONKEY/modules/pwnkit_cve_2021_4034/iamroot_modules.c
T
leviathan 43e290b224 Phase 7: Pwnkit (CVE-2021-4034) detect-only module
First USERSPACE LPE in IAMROOT (every prior module is kernel). Same
iamroot_module interface — the difference is the affected-version
check is package-version-based rather than kernel-version-based.

- modules/pwnkit_cve_2021_4034/:
  - iamroot_modules.{c,h}: detect() locates setuid pkexec (one of
    /usr/bin/pkexec, /usr/sbin/pkexec, /bin/pkexec, /sbin/pkexec,
    /usr/local/bin/pkexec) and parses 'pkexec --version' output.
    Handles BOTH version-string formats: legacy '0.105'/'0.120'
    (older polkit) AND modern bare-integer '121'/'126' (post-0.121
    rename to single-number scheme). Reports VULNERABLE on parse
    failure (conservative).
  - exploit() returns IAMROOT_PRECOND_FAIL with a 'not yet
    implemented' message; full Qualys-PoC follow-up is the next
    commit. ~200 lines including embedded .so generator.
  - MODULE.md documents the bug, affected ranges, distro backport
    landscape (RHEL 7/8, Ubuntu focal/impish, Debian buster/bullseye
    each have their own backported polkit version).
  - Embedded auditd + sigma detection rules:
    auditd: pkexec watch + execve audit
    sigma:  pkexec invocation + suspicious env (GCONV_PATH, CHARSET)

- core/registry.h adds iamroot_register_pwnkit() declaration.
- iamroot.c main() registers pwnkit.
- Makefile gains the pwnkit family as a separate object set.

Verified end-to-end on kctf-mgr (modern polkit 126):
  iamroot --list  → 8 modules
  iamroot --scan  → pwnkit reports 'version 126 ≥ 0.121 (fixed)'
  iamroot --detect-rules --format=auditd | grep pwnkit → emits
2026-05-16 20:07:40 -04:00

195 lines
6.9 KiB
C

/*
* pwnkit_cve_2021_4034 — IAMROOT module
*
* STATUS: 🔵 DETECT-ONLY (2026-05-16). Full exploit follows.
*
* Detect: check pkexec presence + version. The fix landed in
* polkit 0.121. Distros backport to various polkit versions, so a
* naive "polkit < 0.121 == vulnerable" rule overcounts. We check
* pkexec's reported version and the distro's polkit package version
* if we can.
*
* Exploit: stubbed. The canonical Qualys PoC (~200 lines + an
* embedded .so generator) is well-documented; landing it is a
* follow-up commit.
*
* Pwnkit is the first USERSPACE LPE in IAMROOT — the rest of the
* corpus is kernel bugs. The module shape is identical (same
* iamroot_module interface), but the affected-version check is
* package-version-based rather than kernel-version-based. core/
* may eventually grow a `pkg_version` helper if a few more userspace
* modules need it.
*/
#include "iamroot_modules.h"
#include "../../core/registry.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
static const char *find_pkexec(void)
{
static const char *candidates[] = {
"/usr/bin/pkexec",
"/usr/sbin/pkexec",
"/bin/pkexec",
"/sbin/pkexec",
"/usr/local/bin/pkexec",
NULL,
};
for (size_t i = 0; candidates[i]; i++) {
struct stat st;
if (stat(candidates[i], &st) == 0) {
/* setuid bit is the marker for a vulnerable install */
if (st.st_mode & S_ISUID) return candidates[i];
}
}
return NULL;
}
/* Returns true if version_str represents a vulnerable polkit
* (< 0.121 fix). Handles both formats:
* Older polkit: "0.105", "0.120" → vulnerable if minor < 121
* Modern polkit: bare integer "121", "122", "126" → vulnerable if < 121
* Caveat: distro backports may have fixed lower-numbered versions;
* we conservatively report VULNERABLE on parse failure rather than
* silently passing. */
static bool pkexec_version_vulnerable(const char *version_str)
{
int maj = 0, min = 0;
int n = sscanf(version_str, "%d.%d", &maj, &min);
if (n < 1) return true; /* can't parse → assume worst */
if (n == 1) {
/* Bare integer (modern polkit): "121", "126", etc. */
return maj < 121;
}
/* "X.Y" format (older polkit) */
if (maj > 0) return false; /* 1.x or higher = post-fix */
return min < 121; /* 0.121 is the fix */
}
static iamroot_result_t pwnkit_detect(const struct iamroot_ctx *ctx)
{
const char *pkexec_path = find_pkexec();
if (!pkexec_path) {
if (!ctx->json) {
fprintf(stderr, "[+] pwnkit: pkexec not installed; no attack surface\n");
}
return IAMROOT_OK;
}
if (!ctx->json) {
fprintf(stderr, "[i] pwnkit: found setuid pkexec at %s\n", pkexec_path);
}
/* Run `pkexec --version` and parse. We pipe stderr/stdout to a
* temp file because popen() can have quoting quirks. */
char cmd[512];
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", pkexec_path);
FILE *p = popen(cmd, "r");
if (!p) return IAMROOT_TEST_ERROR;
char line[256] = {0};
char *r = fgets(line, sizeof line, p);
pclose(p);
if (!r) {
if (!ctx->json) {
fprintf(stderr, "[?] pwnkit: could not parse pkexec --version output\n");
}
return IAMROOT_TEST_ERROR;
}
/* Output format: "pkexec version 0.105\n" or "pkexec version 0.120-..." */
char *vp = strstr(line, "version");
if (!vp) return IAMROOT_TEST_ERROR;
vp += strlen("version");
while (*vp == ' ' || *vp == '\t') vp++;
if (!ctx->json) {
char *nl = strchr(vp, '\n');
if (nl) *nl = 0;
fprintf(stderr, "[i] pwnkit: pkexec reports version '%s'\n", vp);
}
bool vuln = pkexec_version_vulnerable(vp);
if (vuln) {
if (!ctx->json) {
fprintf(stderr, "[!] pwnkit: pkexec version is pre-0.121 fix → likely VULNERABLE\n");
fprintf(stderr, "[i] pwnkit: distro backports may have fixed lower-numbered versions;\n"
" check `apt-cache policy policykit-1` / `rpm -q polkit` for the patch level\n");
}
return IAMROOT_VULNERABLE;
}
if (!ctx->json) {
fprintf(stderr, "[+] pwnkit: pkexec version is ≥ 0.121 (fixed)\n");
}
return IAMROOT_OK;
}
static iamroot_result_t pwnkit_exploit(const struct iamroot_ctx *ctx)
{
(void)ctx;
fprintf(stderr,
"[-] pwnkit: exploit not yet implemented in IAMROOT.\n"
" Status: 🔵 DETECT-ONLY (see CVES.md, ROADMAP.md Phase 7).\n"
" The canonical Qualys PoC (~200 lines + embedded .so generator)\n"
" is the reference; landing it in iamroot_module form is the\n"
" Phase 7 follow-up. For now, --scan correctly reports per-host\n"
" vulnerability; run Qualys' public PoC manually to verify.\n");
return IAMROOT_PRECOND_FAIL;
}
/* ----- Embedded detection rules ----- */
static const char pwnkit_auditd[] =
"# Pwnkit (CVE-2021-4034) — auditd detection rules\n"
"# Flag pkexec execution from non-root + look for argc==0 indicators.\n"
"-w /usr/bin/pkexec -p x -k iamroot-pwnkit\n"
"-a always,exit -F arch=b64 -S execve -F path=/usr/bin/pkexec -k iamroot-pwnkit-execve\n"
"-a always,exit -F arch=b32 -S execve -F path=/usr/bin/pkexec -k iamroot-pwnkit-execve\n";
static const char pwnkit_sigma[] =
"title: Possible Pwnkit exploitation (CVE-2021-4034)\n"
"id: 9e1d4f2c-iamroot-pwnkit\n"
"status: experimental\n"
"description: |\n"
" Detects pkexec invocations with GCONV_PATH / CHARSET env tweaks (the\n"
" Qualys PoC pattern). Also flags any execve(pkexec) where argv0 is\n"
" empty or NULL (which is the bug's hallmark trigger).\n"
"logsource: {product: linux, service: auditd}\n"
"detection:\n"
" pkexec_invocation:\n"
" type: 'EXECVE'\n"
" exe|endswith: '/pkexec'\n"
" suspicious_env:\n"
" - 'GCONV_PATH='\n"
" - 'CHARSET='\n"
" - 'PATH=GCONV_PATH=.'\n"
" condition: pkexec_invocation and suspicious_env\n"
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2021.4034]\n";
const struct iamroot_module pwnkit_module = {
.name = "pwnkit",
.cve = "CVE-2021-4034",
.summary = "pkexec argv[0]=NULL → env-injection LPE (polkit ≤ 0.120)",
.family = "pwnkit",
.kernel_range = "userspace bug — affects polkit ≤ 0.120; pkexec setuid-root binary",
.detect = pwnkit_detect,
.exploit = pwnkit_exploit,
.mitigate = NULL, /* mitigation = upgrade polkit / chmod -s pkexec */
.cleanup = NULL, /* no per-exploit cleanup once full impl lands */
.detect_auditd = pwnkit_auditd,
.detect_sigma = pwnkit_sigma,
.detect_yara = NULL,
.detect_falco = NULL,
};
void iamroot_register_pwnkit(void)
{
iamroot_register(&pwnkit_module);
}