0x00 作者
钱程 of IceSword Lab
0x01 漏洞基本信息
polkit 的 pkexec 程序中存在一个本地权限提升漏洞。当前版本的 pkexec 无法正确处理调用参数计数,并最终尝试将环境变量作为命令执行。攻击者可以通过控制环境变量来利用这一点,从而诱导 pkexec 执行任意代码。利用成功后,会导致本地特权升级,非特权用户获得管理员权限
软件简介
polkit 是一个应用程序级别的工具集,通过定义和审核权限规则,实现不同优先级进程间的通讯:控制决策集中在统一的框架之中,决定低优先级进程是否有权访问高优先级进程。
Polkit 在系统层级进行权限控制,提供了一个低优先级进程和高优先级进程进行通讯的系统。和 sudo 等程序不同,Polkit 并没有赋予进程完全的 root 权限,而是通过一个集中的策略系统进行更精细的授权。
Polkit 定义出一系列操作,例如运行 GParted, 并将用户按照群组或用户名进行划分,例如 wheel 群组用户。然后定义每个操作是否可以由某些用户执行,执行操作前是否需要一些额外的确认,例如通过输入密码确认用户是不是属于某个群组。
https://wiki.archlinux.org/title/Polkit_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)
漏洞原理概括
当前版本的 pkexec 无法正确处理调用参数计数,并最终尝试将环境变量作为命令执行。攻击者可以通过控制环境变量来利用这一点,从而诱导 pkexec 执行任意代码。
前置知识
pkexec 是 polkit 的一个程序,可以以其他用户身份执行命令。
1 | ➜ pkexec --help |
不指定 --user
参数时,缺省为 root
。比如:
1 | pkexec reboot |
漏洞环境搭建
环境没有特殊要求,主流 Linux 发行版都可以。
本次测试的环境:
1 | ➜ uname -a |
0x02 漏洞分析
对该漏洞的分析将结合已知的 POC 和 Qualys 的报告进行。
分析 POC
先来分析 POC:
1 | 1 |
在该 POC 中:
- L5-L15,即 payload,引入了一个 root 权限的
/bin/sh
- L19,创建目录
GCONV_PATH=.
,创建文件GCONV_PATH=./pwnkit
并添加了执行权限 - L20,创建目录
pwnkit
,创建文件pwnkit/gconv-modules
并写入内容module UTF-8// PWNKIT// pwnkit 2
- L21-L24,把 payload 写入
pwnkit/pwnkit.c
并编译为动态链接库pwnkit/pwnkit.so
- L25,一个特殊的数组
- L26,使用
execve
调用pkexec
,这里有个特别的参数(char*[]){NULL}
,这也是整个 POC 的启动点
测试一下 POC:
奇妙的 argc 为 0
argc 和 argv 大家都熟悉,为了后面的分析这里再介绍一下:
- argc:即 argument count,保存运行时传递给 main 函数的参数个数。
- argv:即 argument vector,保存运行时传递 main 函数的参数,类型是一个字符指针数组,每个元素是一个字符指针,指向一个命令行参数。
例如: - argv[0] 指向程序运行时的全路径名;
- argv[1] 指向程序在命令行中执行程序名后的第一个字符串
下面的代码就展示了 argc 和 argv 用法:
1 | //t.c |
execve()
execve() 可以执行程序,使用该函数需要引入 unistd.h
头文件,函数原型:
1 | int execve(const char *pathname, char *const argv[], |
我们使用前面的 t.c
来熟悉一下 execve()
:
1 | //ex.c |
前面 POC 中 L26,使用了 execve()
:
1 | 25 char *env[] = { "pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=pwnkit", NULL }; |
但是参数使用方法和我们测试的不同,第二个参数使用了 (char*[]){NULL}
进行填充。我们来测试一下这样会有什么结果:
1 | //ex.c |
此时我们发现 argc 为 0,且 argv[0] 内容为空,不再是程序本身。这有什么用呢?用处很大。
pkexec 中的越界读取
现在来分析 pkexec 的代码,其 main() 函数主要结构如下:
1 | 435 main (int argc, char *argv[]) |
其中有两个 glib 提供的函数 g_strdup() 和 g_find_program_in_path() ,先熟悉一下:
g_strdup()
复制一个字符串,声明如下:
1
2 gchar *
g_strdup (const gchar *str);g_find_program_in_path()
在用户路径中定位第一个名为 program 的可执行程序,与 execvp() 定位它的方式相同。返回具有绝对路径名的已分配字符串,如果在路径中找不到程序,则返回 NULL。如果 program 已经是绝对路径,且如果 program 存在并且可执行,则返回 program 的副本,否则返回 NULL。
1
2
3
4 gchar*
g_find_program_in_path (
const gchar* program
)
再看 main() 函数中:
- L534-L568,用来处理命令行参数
- L534:n=1,当 argc=1 时,不会进入循环,比如:
pkexec
;当 argc>1时,才会进入循环,比如:pkexec --version
- L534:n=1,当 argc=1 时,不会进入循环,比如:
- L610-L640,如果其路径不是绝对路径,会在 PATH 中搜索要执行的程序
- L610:使用
g_strdup()
复制argv[n]
的内容到path
,因为在pkexec
中argv[n]
就是目标路径,比如:pkexec reboot
- L629:这里判断是否是绝对路径的方法比较巧妙,使用
path[0] != '/'
来判断 - L632:检索目标路径,返回目标路径字符串
- L639:将返回的路径赋值给
path
和argv[n]
- L610:使用
正常情况下,这样处理的逻辑没有问题。
但如果命令行参数 argc 为 0,则会出现意外情况:
- L534,n 永久设置为 1;
- L610,
argv[1]
发生越界读取,并把越界读取到的值赋给了path
; - L639,指针 s 被越界写入
argv[1]
。
问题在于,这个越界的 argv[1]
中读取和写入的是什么?
我们需要先了解参数的内存布局,结合内核代码来分析:
1 | // linux5.4/fs/binfmt_elf.c: |
从代码中可以看出,当 execve() 一个新程序时,内核将参数、环境字符串和指针(argv 和 envp)复制到新程序堆栈的末尾,main 函数参数是布局在栈上,argc、argv依次入栈(L307、L321),后面紧接着就是 env 入栈(L325-L336)。
把上面的代码简化成下面的图示:
1 | |---------+---------+-----+------------|---------+---------+-----+------------| |
可以发现 argv 和 envp 指针在内存中是连续的,如果 argc 为 0,那么越界 argv[1] 实际上是 envp[0]
,指向第一个环境变量 value
的指针。
argv[1] 是什么解决了,那再回过来看 pkexec 的 main() 函数
1 | 435 main (int argc, char *argv[]) |
- L610,要执行的程序的路径从 argv[1](即
envp[0]
)越界读取,并指向value
- L632,这个路径
value
被传递给g_find_program_in_path()
g_find_program_in_path()
会在 PATH 环境变量的目录中搜索一个名为value
的可执行文件- 如果找到这样的可执行文件,则将其完整路径返回给 pkexec 的 main() 函数(L632)
- 最后,L639,这个完整路径被越界写入 argv[1](即
envp[0]
),覆盖了第一个环境变量。
因此只要能控制 g_find_program_in_path
返回的字符串,就可以注入任意的环境变量。
Qualys 指出如果 PATH 环境变量是 PATH=name
,并且目录 name
存在(在当前工作目录中)并且包含一个名为 value
的可执行文件,则写入一个指向字符串 name/value
的指针越界到 envp[0]
。
进一步,让这个组合的文件名里包含等号 “=”。传入 PATH=name=.
,创建一个 name=.
目录,并在其中放一个可执行文件 value
,最终 envp[0]
就会被篡改为 name=./value
,也就是注入了一个新的环境变量进去。
换句话说,这种越界写入可以绕过原有的安全检查,将不安全的环境变量(例如,LD_PRELOAD)重新引入 pkexec 的环境。
寻找不安全的环境变量
新的问题是:要成功利用这个漏洞,应该将哪个不安全变量重新引入 pkexec 的环境中?我们的选择是有限的,因为在越界写入后不久(L639),pkexec 完全清除了它的环境(L702):
1 | 639 argv[n] = path = s; |
答案来自于 pkexec 的复杂性:为了向 stderr 打印错误消息,pkexec 调用 GLib 的函数 g_printerr()
(注意:GLib 是 GNOME 库,而不是 GNU C 库,即 glibc);例如,函数 validate_environment_variable()
和 log_message()
调用 g_printerr()
(L126,L408-L409):
1 | 88 log_message (gint level, |
g_printerr()
通常打印 UTF-8 错误消息,但如果环境变量 CHARSET 不是 UTF-8,它可以打印另一个字符集中的消息(注意:CHARSET 不是安全敏感的,它不是不安全的环境变量)。
要将消息从 UTF-8 转换为另一个字符集,g_printerr()
调用 glibc 的函数 iconv_open()
。
要将消息从一个字符集转换为另一个字符集,iconv_open()
执行小型共享库;通常,这些三元组(“from”字符集、“to”字符集和库名称)是从默认配置文件 /usr/lib/gconv/gconv-modules
中读取的。但环境变量 GCONV_PATH
可以强制 iconv_open()
读取另一个配置文件;所以 GCONV_PATH
是不安全的环境变量之一(因为它会导致执行任意库),因此会被 ld.so 从 SUID 程序的环境中删除。
我们可以把 GCONV_PATH
重新引入 pkexec 的环境,并以 root 身份执行我们自己的共享库。
回顾 POC
现在我们对漏洞原理有了更深的认识,再看一看 POC
1 | 1 |
需要新注意的是:
- L26,使用
execve
调用pkexec
,(char*[]){NULL}
造成argv[1]
越界读取 - L25,一个特殊的数组,env[0]为 payload,env[1]引入了
GCONV_PATH
- L20,设置非UTF-8环境,也就导致 payload 中
gconv_init
执行,造成/bin/sh
执行,恢复环境变量得到 root shell。
0x03 漏洞总结
总结一下该漏洞的利用思路:
- 通过设置
execve()
的 argv[] 为零,造成 argv[1] 越界读取,并绕过安全检查 - 通过
g_printerr
函数发现可控的不安全环境变量GCONV_PATH
- 构造畸形的路径使
pkexec
从指定路径读取环境变量完成提权
这个漏洞的质量非常好,利用思路也很有趣,借用一下 Qualys 对该漏洞的评价:
这个漏洞是攻击者的梦想成真。
- pkexec 默认安装在所有主要的 Linux 发行版上(我们利用了 Ubuntu、Debian、Fedora、CentOS,而其他发行版也可能利用)
- pkexec 自 2009 年 5 月创建以来就存在漏洞(commit c8c3d83, “Add a pkexec(1) command”)
- 任何没有特权的本地用户都可以利用这个漏洞来获得完全的 root 权限。
- 虽然这个漏洞在技术上是一个内存损坏,但它可即时、可靠地、以独立于架构的方式加以利用。
- 即使 polkit 守护进程本身没有运行,也可以利用。
0x04 漏洞补丁
a. 如何检测该漏洞
检查组件版本:
1 | ➜ pkexec --version |
b. 如何防御该漏洞
及时升级组件
c. 有没有哪种通用的缓解措施可以阻断该漏洞
Qualys 在 报告 中给出了缓解措施:
1 | # chmod 0755 /usr/bin/pkexec |
即从 pkexec 中删除 SUID 位
RedHat 给出了针对该漏洞的缓解措施:
https://access.redhat.com/security/vulnerabilities/RHSB-2022-001
0x05 参考
- CVE-2021-4034 pkexec 本地提权 - 非尝咸鱼贩 [2022-01-26]
- PwnKit: Local Privilege Escalation Vulnerability Discovered in polkit’s pkexec (CVE-2021-4034) | Qualys Security Blog
- https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt
- 演示视频:PwnKit Vulnerability on Vimeo
- Commit:pkexec: local privilege escalation (CVE-2021-4034) (a2bf5c9c) · Commits · polkit / polkit · GitLab
- POC:arthepsy/CVE-2021-4034: PoC for PwnKit: Local Privilege Escalation Vulnerability Discovered in polkit’s pkexec (CVE-2021-4034)
- oss-security - pwnkit: Local Privilege Escalation in polkit’s pkexec (CVE-2021-4034)
- CVE-2021-4034:Linux Polkit 权限提升漏洞通告 - 360CERT [2022-01-26]
更早的相关研究
- Privilege escalation with polkit: How to get root on Linux with a seven-year-old bug | The GitHub Blog [2021-06-10]
- argv silliness | ~ryiron [2013-12-16]
- pkexec - Race Condition Privilege Escalation (CVE-2011-1485) - Linux local Exploit [2011-10-08]
- glibc locale issues - Tavis Ormandy [2014-07-14]
- charset.alias in pkexec/glib/gnulib (was: glibc locale issues) - Jakub Wilk [2017-06-23]
- Getting Arbitrary Code Execution from fopen’s 2nd Argument | The Pwnbroker [2019-11-04]
- Simple Bugs and Vulnerabilities in Linux Distributions - Silvio Cesare [2011-03-25]