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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user