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 权限,而是通过一个集中的策略系统进行更精细的授权。其系统架构由授权和身份验证代理组成,如下图所示:
其中授权以基于system message bus上的服务实现,polkit没有取代系统已有的权限系统,而是在已有的群组和管理员上进行管控。如果开发时需要用到linux上的身份认证,可以基于polkit框架,编写rule授权规则实现。
而pkexec就是polkit中的一个组件,允许用户以另一个用户身份执行命令(用法类似于sudo):
1 | └─# pkexec --help |
如果 PROGRAM未指定,将运行默认shell,如果username未指定,则程序将以管理超级用户root身份执行。
2.2 Polkit source installation
这里安装Polkit漏洞环境,参照了官方的源码编译流程,方便调试
1 | git clone https://gitlab.freedesktop.org/polkit/polkit/ |
meson setup后会有如下的提示,如果出现该提示代表成功了
在meson compile后可以在install目录下找到pkexec可执行文件,之后就可以进行源码调试了。
2.3 suid
SUID (Set owner User ID up on execution) 是Linux中的一种特殊权限,其功能为用户运行某个程序时,如果该程序有SUID权限,那么程序运行为进程时,进程的属主不是发起者,而是程序文件所属的属主。但是SUID权限的设置只针对二进制可执行文件,对于非可执行文件设置SUID没有任何意义.
在执行过程中,调用者会暂时获得该文件的所有者权限,且该权限只在程序执行的过程中有效。 通俗的来讲,假设我们现在有一个可执行文件ls
,其属主为root,当我们通过非root用户登录时,如果ls
设置了SUID权限,我们可在非root用户下运行该二进制可执行文件,在执行文件时,该进程的权限将为root权限。
我们简单看一下如何设置SUID权限
1 | chmod u+s filename 设置SUID位 |
ls -al
查看文件权限
1 | chmod u+s 1 |
可以看到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]导致内存溢出。
上面对补丁做了简单的分析,之后对漏洞的成因进行分析。
3.2 Root Cause Analysis
这里的根因分析主要参考了 https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt 与 https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt
上面已经提到过:pkexec允许用户以另一个用户身份执行PROGRAM。如果未指定 PROGRAM,则将运行默认 shell。如果未指定用户名,则程序将以 root 身份执行。
pkexec.c的 534-568行是pkexec处理命令行参数的代码,在 610-640 行代码中,会获取PROGRAM参数名称,也就是需要执行的程序。
这里会存在一个问题,如果命令行参数 argc 的数量为 0( execve() 的 argv 参数为空,即 {NULL}),那么argv[0] 为 NULL。则会导致:
在第 534 行,整数 n 永久设置为 1;
在第 610 行,由于argv只有一个元素 argv[0], argv[1] 将会越界读取指针路径;
在第 639 行,指针 s 被越界写入 argv[1]。
1 | 435 main (int argc, char *argv[]) |
这时我们得到了一个越界读写,也就是argv[1]的位置,那么argv[1]是什么呢,先说结论,argv与envp的内存空间是紧邻的,也就是说argv的最后一个元素argv[lartest]与envp的第一个元素envp[0]内存空间相邻。内存布局如下:
1 | |---------+---------+-----+------------|---------+---------+-----+------------| |
编写代码验证下,直接参考 saucerman师傅博客 的代码。
1 | // a.c |
编译debug模式调试也可以验证,argv[lartest]与envp[0]的内存是紧邻的。
顺着这个思路,再次回看代码
- 假设我们执行pkexec,此时argc={ NULL },envp={“xxx”}
- 610行,程序会读取argv[1]到path变量中,也就是”xxx”
- 632行,
s = g_find_program_in_path (path)
找到该程序的绝对路径,假设为/usr/bin/xxx - 639行,程序将s写入argv[1]和path,从而覆盖了第一个环境变量。此时envp[0]也就变成了{“/usr/bin/xxx”}
1 | 435 main (int argc, char *argv[]) |
也就是说,这种越界写入允许我们将一个“不安全”的环境变量(例如,LD_PRELOAD)重新引入pkexec的环境。但是这个环境变量并不会存在太久,因为在702行,程序会清除所有的环境变量:
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 | /* |
该exp的流程大致为
- 编译payload.c 为 payload.so。
- 创建目录
GCONV_PATH=.
并在该目录中创建文件lol
。 - 创建目录
lol
并在该目录中创建配置文件gconv-modules
,之后在该文件中写入内容(该内容指定了生成的so文件)。 - 执行pkexec程序,不传入参数,传入一些特殊的环境变量。
- 反弹root shell
我们先将该exp编译执行
之后我们通过调试该exp来更加理解该漏洞的利用技巧。
首先610行读取argv[1],实际上读取的是envp[0]。造成越界读取。
629行如果传入的命令不是绝对路径,那么将会在632行调用 g_find_program_in_path
函数在 PATH 环境变量的目录里搜索。632行这里由于我们指定了环境变量PATH=GCONV_PATH=.
,所以path将会被赋值为 GCONV_PATH=./lol
。
这里会有一个疑问,为什么要通过PATH环境变量传入 GCONV_PATH=./lol
。直接在第一个环境变量传入 GCONV_PATH=./lol
不是更方便吗,经过网上学习师傅的文章以及动手实践得到了一个结论:某些敏感的环境变量将会被清除。
下面的两步调试基于在第一个环境变量传入 GCONV_PATH=./lol
的代码(错误的exp代码)。调试过程如下,在execve执行过程中,GCONV_PATH 还没有被清除
但是到达pkexec.c的main函数时,GCONV_PATH 已经被清除了
所以需要通过 PATH环境变量
传入危险环境变量 GCONV_PATH
。
接下来回到正确的exp代码,639行 envp[0] 被赋值为GCONV_PATH=./lol
。
670行后调用 validate_environment_variable 函数,此时的value 为 “en_US.UTF-8”
这里可以拿到 GCONV_PATH 环境变量的值,也就是 gconv-modules 文件的路径。
iconv_open函数会读取环境变量中的GCONV_PATH的路径下的gconv-modules文件。如下,gconv_conf_filename 为静态常量,名称固定为”gconv-modules”。
之后读取文件内容。
获得module名称。
最后就是执行的过程了
这里的step_cnt 为0, result[0]为payload.so,执行里面的gconv_init即执行了恶意代码。
四、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函数
修补前
修补后
或者可以通过退出码辅助判断
修补前
修补后
可以看到修补后有退出码127,目的就是为了该漏洞做的判断
在我们做实战攻防的过程中,如果不能判断机器是否能够用该漏洞提权,除了直接拿exp打,也可以通过逆向来判断。一是判断pkexec是否有suid,二是判断pkexec是否打了补丁。
五、Reference link
- https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html # polkit Official Manual
- https://saucer-man.com/information_security/876.html # Vulnerability Analysis
- https://gitlab.freedesktop.org/polkit/polkit/-/commit/a2bf5c9c83b6ae46cbd5c779d3055bff81ded683 # Patch link
- https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt # Vulnerability details public
- https://en.wikipedia.org/wiki/Setuid # suid wikipedia
- https://xz.aliyun.com/t/10870#toc-2 # Vulnerability Analysis
- https://gitlab.freedesktop.org/polkit/polkit/ # polkit Source Code