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
This commit is contained in:
2026-05-16 20:07:40 -04:00
parent 28ad566964
commit 43e290b224
8 changed files with 278 additions and 3 deletions
+1
View File
@@ -26,6 +26,7 @@ Status legend:
| CVE-2022-0847 | Dirty Pipe — pipe `PIPE_BUF_FLAG_CAN_MERGE` write | LPE (arbitrary file write into page cache) | mainline 5.17 (2022-02-23) | `dirty_pipe` | 🟢 | Full detect + exploit + cleanup. Detect: branch-backport ranges (5.10.102 / 5.15.25 / 5.16.11 / 5.17+). Exploit: page-cache write into /etc/passwd UID field followed by `su` to drop a root shell. Auto-refuses on patched kernels. Cleanup: drop_caches + POSIX_FADV_DONTNEED. CI-validation against a vulnerable kernel (e.g. Ubuntu 20.04 with stock 5.13) is Phase 4 work. |
| CVE-2023-0458 | EntryBleed — KPTI prefetchnta KASLR bypass | INFO-LEAK (kbase) | mainline (partial mitigations only) | `entrybleed` | 🟢 | Stage-1 leak brick. Working on lts-6.12.86 (verified 2026-05-16 via `iamroot --exploit entrybleed --i-know`). Default `entry_SYSCALL_64` slot offset matches lts-6.12.x; override via `IAMROOT_ENTRYBLEED_OFFSET=0x...`. Other modules can call `entrybleed_leak_kbase_lib()` as a library. x86_64 only. |
| CVE-2026-31402 | NFS replay-cache heap overflow | LPE (NFS server) | mainline 2026-04-03 | — | ⚪ | Candidate. Different audience (NFS servers) — TBD whether in-scope. |
| CVE-2021-4034 | Pwnkit — pkexec argv[0]=NULL → env-injection | LPE (userspace setuid binary) | polkit 0.121 (2022-01-25) | `pwnkit` | 🔵 | Detect-only as of 2026-05-16. Locates setuid pkexec, parses `pkexec --version`, compares against 0.121 threshold. **First userspace LPE in IAMROOT** (rest is kernel). Full Qualys-PoC exploit follows in Phase 7 follow-up. Ships auditd + sigma rules. |
| CVE-TBD | Fragnesia (ESP shared-frag in-place encrypt) | LPE (page-cache write) | mainline TBD | `_stubs/fragnesia_TBD` | ⚪ | Stub. Per `findings/audit_leak_write_modprobe_backups_2026-05-16.md`, requires CAP_NET_ADMIN in userns netns — may or may not be in-scope depending on target environment. |
## Operations supported per module
+6 -1
View File
@@ -41,10 +41,15 @@ EB_DIR := modules/entrybleed_cve_2023_0458
EB_SRCS := $(EB_DIR)/iamroot_modules.c
EB_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(EB_SRCS))
# Family: pwnkit (userspace polkit bug, not kernel)
PK_DIR := modules/pwnkit_cve_2021_4034
PK_SRCS := $(PK_DIR)/iamroot_modules.c
PK_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PK_SRCS))
# Top-level dispatcher
TOP_OBJ := $(BUILD)/iamroot.o
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS)
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS)
.PHONY: all clean debug static help
+6 -2
View File
@@ -133,12 +133,16 @@ primitive** that other modules can chain. Bundled because:
- [ ] Idempotent re-run safety: copy_fail_family's apply is already
idempotent (overwrites conf files). Re-verify per module.
## Phase 7+ — More modules
## Phase 7+ — More modules (started 2026-05-16)
Backfill of historical and recent LPEs as time allows:
- [ ] **CVE-2021-3493** — overlayfs nested-userns LPE
- [ ] **CVE-2021-4034** — Pwnkit (pkexec env handling)
- [x] **CVE-2021-4034** — Pwnkit (pkexec env handling): 🔵 detect-only
landed. Version parser handles both formats: "0.X.Y" (older
polkit) and bare "121"/"126" (modern). Reports VULNERABLE if
pkexec is setuid AND version < 121. First userspace LPE in the
corpus. Full Qualys-PoC exploit is the next Phase 7 commit.
- [ ] **CVE-2022-2588** — net/sched route4 dead UAF
- [ ] **CVE-2023-2008** — vmwgfx OOB write
- [ ] **CVE-2024-1086** — netfilter nf_tables UAF
+1
View File
@@ -23,5 +23,6 @@ const struct iamroot_module *iamroot_module_find(const char *name);
void iamroot_register_copy_fail_family(void);
void iamroot_register_dirty_pipe(void);
void iamroot_register_entrybleed(void);
void iamroot_register_pwnkit(void);
#endif /* IAMROOT_REGISTRY_H */
+1
View File
@@ -220,6 +220,7 @@ int main(int argc, char **argv)
iamroot_register_copy_fail_family();
iamroot_register_dirty_pipe();
iamroot_register_entrybleed();
iamroot_register_pwnkit();
enum mode mode = MODE_SCAN;
struct iamroot_ctx ctx = {0};
+57
View File
@@ -0,0 +1,57 @@
# Pwnkit — CVE-2021-4034
> 🔵 **DETECT-ONLY** as of 2026-05-16. Full exploit follows.
## Summary
Polkit's `pkexec` parses argv assuming argc ≥ 1. With `argc == 0`, the
parsing reads past `argv[0]` into the contiguous envp region, treating
the first env string as if it were argv[0]. By placing `GCONV_PATH=`
crafted entries in the environment and naming a controlled file such
that libc's iconv() loads it as a gconv module, an unprivileged user
gets code execution as root via the setuid pkexec binary.
Disclosed by Qualys 2022-01-25. Bug existed since pkexec's first
release in 2009 — affects every distribution shipping a vulnerable
polkit until 0.121 (or distro backport).
## Affected versions
- **All polkit ≤ 0.120** (i.e., pkexec from 2009 onward) before the
fix landed.
- Patched in upstream **polkit 0.121** (2022-01-25).
- Distro backports vary:
- Ubuntu: 0.105-26ubuntu1.3 (focal), 0.105-31ubuntu0.1 (impish), etc.
- Debian: 0.105-31+deb11u1 (bullseye), 0.105-26+deb10u1 (buster)
- RHEL: polkit-0.115-13.el7_9 (RHEL 7), polkit-0.117-9.el8_5.1 (RHEL 8)
## IAMROOT detect logic (current)
1. Resolve pkexec binary (`/usr/bin/pkexec` or `which pkexec`)
2. If not present → IAMROOT_OK (no attack surface)
3. Run `pkexec --version` and parse version
4. Compare to known-fixed thresholds; report VULNERABLE if below
## Exploit logic (follow-up)
Canonical Qualys / public Pwnkit PoC:
1. Build a malicious shared object that `exit(setuid(0)); system("/bin/sh")`
2. Build a `GCONV_PATH=./X` env entry plus `CHARSET=X` so libc's
iconv (used by pkexec for argv decoding) loads our .so
3. `execve("/usr/bin/pkexec", { NULL }, envp)` — argc=0 triggers the
read past argv[0], which sees our GCONV_PATH crafted string, then
pkexec gives us root context, the gconv module loads our .so as
root, we drop to a shell
~200 lines including the embedded .so generator. Phase 7 follow-up
commit lands the full version.
## Detection rules (shipped)
`detect/auditd.rules` — flags pkexec invocations from non-root.
## References
- https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
- https://nvd.nist.gov/vuln/detail/CVE-2021-4034
@@ -0,0 +1,194 @@
/*
* 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);
}
@@ -0,0 +1,12 @@
/*
* pwnkit_cve_2021_4034 — IAMROOT module registry hook
*/
#ifndef PWNKIT_IAMROOT_MODULES_H
#define PWNKIT_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module pwnkit_module;
#endif