pkexec与DirtyPipe的提权原理

每次提权的时候都是找来个EXP,提不了-->放弃。但是对方的服务器真的没漏洞吗?还是说我们的操作有些不对或者写EXP的作者可能没有更深入的研究导致EXP的适用性出现了些许的问题。为了弄清这些,我们不得不深入的学习一下这些漏洞的原理。

polkit

CVE-2021-4034

也就是我们俗称的pkexec

修复版本

前提介绍

polkit是一个授权管理器,其系统架构由授权和身份验证代理组成,pkexec是其中polkit的其中一个工具,他的作用有点类似于sudo,允许用户以另一个用户身份执行命令

源码链接:https://gitlab.freedesktop.org/polkit/polkit/-/blob/0.120/src/programs/pkexec.c

image-20230107201340067

这里main函数进来以后,先要对运行pkexec的参数进行处理,n=1被初始化赋值

但如果直接运行pkexec,没有加任何参数,这里的n也就一直为1了

C语言中,不传递任何参数,argc为1:

image-20230107201814383

继续来到第610行

image-20230107220330057

此时argv[1](不输入参数,则应该只有argv[0])实际上是越界了的,因为没有输入参数,而根据栈的布局,main函数输入的参数argv和envp是挨在一起的,这个稍微有一点二进制调试经验的人都知道

image-20230107202136433

也就是说此时的argv[1]指向的是envp[0] 因此 path 会被赋值为envp[0]

如果上面的话你理解不了,我们来看看这个例子:

T3:模拟漏洞程序,就干一件事,打印自己的argv[1] && envp[0]

T4:模拟输入,主要为了给大家展示上面的这句话也就是说此时的argv[1]指向的是envp[0]

image-20230107220234792

Envp[0]的值:

image-20230107202711069

接着来到632行

会通过PATH环境变量找到该程序的绝对路径并返回

最后触发数组下标越界写的地方在639行

此时argv[1]被赋值为 一个绝对地址,也就是 envp[0]被赋值为一个绝对地址 通过上面的分析可以发现,如果攻击者执行pkexec时,指定了恶意的envp[0],那么可以写入一个环境变量到目标进程空间中

然后我们需要找到一个可以利用的环境变量,然后有这种方法给他写进去,这种环境变量是可以导致外部引入so并且执行其中的函数,这个变量就是GCONV_PATH

WHY GCONV_PATH

这一节我们探索为何要写入的是这个环境变量

寻找这个触发点是有限制的,往后阅读会发现,这个环境变量并非长存的,在702行就消失了

image-20230107203605775

于是我们需要在639~702行之间,寻找一个触发点

在源码中引用了很多次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文件实现任意命令执行。

如何触发g_printerr

构造错误的XAUTHORITY环境变量

image-20230107205825953

搜索该函数

这个函数会把两种利用方式都显示在我们的眼前

image-20230107210216414当你构造一个 XAUTHORITY变量,并且内容包含 “..”即可触发g_printerr函数

同理,第二种方法就是构造一个 错误的SHELL变量,就会触发g_printerr函数

利用过程

伪造环境变量所指的目录文件结构

先创建一个名为 GCONV_PATH=. 的目录,然后在这个目录中创建一个 GCONV_PATH=./pwnkit 文件,再创建一个目录pwnkit用于存放恶意的so,然后再创建一个文件 pwnkit/gconv-modules其内容为:

这里有个链接:https://xy2401.com/local-docs/gnu/manual.zh/libc/glibc-iconv-Implementation.html简单讲了module配置文件的写法,简单来说,以上面为例子,意思是从utf-8编码转换成PWNKIT编码,转换所需的资源在pwnkit.so中,消耗cost值为1,这就会让该转换具有更高的优先级

构造恶意so文件

编译之

execve调用pkexec并带入恶意envp数据

有些exp中是没有"SHELL=pwnkit",取而代之的是"XAUTHORITY=../xxx",都差不多,都是为了触发调用g_printerr函数

有些exp中也没有 "CHARSET=PWNKIT",而是 "LC_MESSAGES=en_US.UTF-8",作用都类似 设置完这些环境变量后就调用pkexec

exp分析

在这一节当中我们将认真分析下网上的一些主流EXP,并从中找到一些问题,给出解决方案。

EXP1

复现环境为 Ubuntu1604,pkexec版本为0.105,源码参考:https://gitlab.freedesktop.org/polkit/polkit/-/blob/0.105/src/programs/pkexec.c

运行成功的截图就不放了,大家自行测试就可以,注意各个版本的差异。

exp2

这回我们选用0.115版本的pkexec进行漏洞复现

源码参考:https://gitlab.freedesktop.org/polkit/polkit/-/blob/0.115/src/programs/pkexec.c 我们边整一个0.115的版本,可以用centos的docker下载后拖出来,也可以去这里下载rpm包 https://centos.pkgs.org/8-stream/centos-baseos-x86_64/polkit-0.115-12.el8.x86_64.rpm.html

在0.114之后,我们发现,多了一行:

image-20230107212033851

也正是这一句,导致网上能找到的大部分exp在0.114+以后的版本中无法提权成功 目前GitHub上能找到注意到这个问题的exp就只有下面两个 https://github.com/PeterGottesman/pwnkit-exploit https://github.com/dzonerzy/poc-cve-2021-4034/blob/main/exploit.go

为什么有了这一句setenv会导致大部分exp无法利用呢?先说结论:这是因为setenv会导致 env环境发生迁移,使得数组越界写的漏洞无法注入恶意环境变量到envp中了

可以做个小实验:

T1:

t2

image-20230107212602353可以看到,在t1中注入的环境变量env1,env2,env4都显示出来了 但是我们可以发现t2的环境变量地址已经发生改变了 一般来说,argv数组是和envp相邻挨着排布在栈里面的,然而这里可以看到,t2的环境变量地址已经变成了一个的地址,且在t2中设置的环境变量env3已经不出现在栈里面了,环境变量地址environ已经改变了

这就会导致漏洞无法利用,前面已经分析过了,漏洞是一个数组越界写,写的是栈上的envp[0],而此时envp已经不在栈上了,那么这个漏洞也就无法利用了,注入GCONV_PATH就不能做到了

为什么会发生envp的迁移?答案在glibc源码中

https://code.woboq.org/userspace/glibc/stdlib/setenv.c.html#149

image-20230107213531817

在调用setenv时,如果发现要set的env不存在,那么会调用realloc函数,重新开辟一段空间来存储environ指针,且替代旧的指针

image-20230107213645472

而如果 发现要set的env存在,那么直接复制数据过去即可,不再创建堆空间

因此我们要避免setenv函数导致的envp迁移,所以需要在调用setenv之前,把该环境变量给设置一遍回到t1.c中 把注释打开

为什么要取消注释呢?因为我们重新看t2的代码:

而t1没能设置对应的env3的值,才导致我们上面分析的那些。现在我们手动设置一下这个值,再来看看。

image-20230107214155814

OK,envp没有发生迁移,这样一来,后面的越界写的漏洞才能继续利用

那么我们应该怎么修复一下exp1呢,很简单,手动设置一下环境变量GIO_USE_VFS即可

修复如下:

其实就多了一点:"GIO_USE_VFS="

当然,这个学习这个漏洞能帮助我们更好的理解PHP的一个必备技能:Bypass disablefunctions,但是本篇是针对Linux提权的分析文章,关于PHP Bypass disablefunctions留到下面去讲。

修复

其实就是因为无参数但我们的envp[0]就是argv[1]的问题,那修复起来也好办,如果没参数(argc<1),就退出

image-20230107221628000

image-20230107221640400

其中exit(126) or exit(127)分别代表

126

Command invoked cannot execute

Permission problem or command is not an executable

127

"command not found"

illegal_command

Possible problem with $PATHor a typo

PKEXEC完。

DirtyPipe

2022年的一个漏洞,我们来尝试学习他

漏洞概述

近日,研究人员披露了一个Linux内核本地权限提升漏洞,发现在copy_page_to_iter_pipepush_pipe函数中,新分配的pipe_buffer结构体成员"flags"未被正确地初始化,可能包含旧值PIPE_BUF_FLAG_CAN_MERGE。攻击者可利用此漏洞向由只读文件支持的页面缓存中的页面写入数据,从而提升权限。该漏洞编号为CVE-2022-0847,因漏洞类型和“DirtyCow”(脏牛)类似,亦称为“DirtyPipe”。

漏洞影响

该漏洞需要Linux内核版本>5.8,在 Linux 5.16.11、5.15.25 和 5.10.102 中修复。

漏洞分析

恕我直言,并不能看的很懂,遂直接贴上代码分析文章

https://zhuanlan.kanxue.com/article-17911.htm

总结一下:

splice 系统调用中未清空 pipe_buffer 的标志位,从而将管道页面可写入的状态保留了下来,这给了我们越权写入只读文件的操作。攻击者利用该漏洞可以覆盖任意只读文件中的数据,这样将普通的权限提升至root权限。

漏洞利用

我们主要谈的是这个提权漏洞是怎么利用的。

上面的文章如果原理分析那里像我一样是懵逼的也没关系,我们只需要关注这个漏洞能干什么事就行了

写入任意文件

不过有三个利用条件:

原始利用方式

最开始给出的payload,作者是通过这样的方式进行的:(EXP为任意文件写入。参数:

你需要指定待写的文件,从第几个字符开始写(这个要大于1),写入什么内容)

第一次看这个估计会懵逼,这是啥,在干嘛?这就要补充一下Linux中关于/etc/passwd以及/etc/shadow的一些小秘密了

/etc/passwd && /etc/shadow的爱恨情仇

在提权的文章中,总有人在说一种提权方式:

这个事情就发生在我们的身边,上次CTF的实网攻防的那台Linux,他的提权方式就可以用写入/etc/passwd来实现,原因是错误的写权限配置导致的。当然,这不是本文的重点,感兴趣的小伙伴可以自行查阅。

区别

历史上Linux的前身,一些基于Unix的系统,是没有shadow这个文件的,用户密码的哈希就保存在/etc/passwd的第二个字段。但是/etc/passwd是全局可读的文件,用户的哈希可能被其他用户所读取,所以后来衍生出了/etc/shadow文件。

自此之后,/etc/passwd的第二列通常设置为x表示用户密码保存在/etc/shadow中,而/etc/shadow文件只有root用户可以读取和写入,这样就保护了密码哈希不能被第三方爆破。

/etc/shadow里面保存的信息和/etc/passwd不太一样:这两个文件的第一列和第二列都是用户名和密码,但/etc/shadow的第三列以后主要保存着密码策略,比如密码上次修改的时间、密码过期的时间、密码过期后多久禁用账户等等。

/etc/shadow的第二列也可以是*或!,这代表这个用户是无密码的(也就是不允许通过密码登录)。无密码不等于空密码,如果你想设置一个用户密码是空字符串,那就把第二列留空即可。

提权

那怎么提权呢?很简单,我们还是看这句话:**/etc/passwd的第二列通常设置为x**,**表示用户密码保存在/etc/shadow

那如果我们去掉这个x会发生什么呢?不如做个实验看看

怕把我的Linux搞坏了,就添加个用户吧,看看去掉x会发生什么

去掉x

image-20230107234220994

没错!免密码su!

这是恢复x的场景

image-20230107234334226

换句话说,你只要去掉那个root的x就可以实现免密码切换root用户了

所以现在你看懂这个payload了吗?

将第一个冒号后面的x去掉,并在后面用户描述的位置多加一个字符,补齐文件长度。

注入shellcode提权

这个也是我们讲的那种方式,利用漏洞低权限情况下将Shellcode写入具有suid标志位,如/usr/bin/su的二进制文件中,然后执行/usr/bin/su,shellcode就是创建一个/tmp/sh,将该文件设置suid位,然后文件具体内容就是提升权限并且返回一个shell。执行完被劫持的具有suid位的二进制文件后,恢复原来的二进制内容,最后再去system运行/tmp/sh后,可直接提权返回一个提权后的shell,此时相当于suid提权的用法

https://haxx.in/files/dirtypipez.c

具体的shellcode不贴了,主要看下shellcode干了什么

寻找具有SUID的文件

指定一下就可以了

但是这个漏洞到此就结束了吗?远远没有,这个漏洞还带来了另外一个令人害怕的利用面:Docker逃逸

Docker逃逸漏洞

我了解这件事还是因为P牛在知识星球中提到了这么一件诡异的事:利用该漏洞修改 Docker 内部文件时,其镜像也会发生改变。这可是件大事,我们细细来谈。

利用分析

Docker 逃逸,即从容器内部操作了外部的文件即可引发逃逸。此漏洞从内核态对文件进行修改,从 Docker 本身角度而言,由内核态修改的文件并不会被监控而去修改挂载的 fs,所以从而可以达到修改镜像的效果。

从以上发现的问题延伸,从容器内部可以访问到 Docker 外部的文件,并对该文件进行修改,如果该文件会被执行,那就可以逃逸到宿主机。

image-20230108200921940

思考清楚利用链,现在需要做的就是找到这么一个文件。

Docker 运行后,其内部进程与外部进程有着 namespace 的区分,具体来说,Docker 通过外部命令对内部的操作,首先是由外部建立,再通过修改 namespace 来完成的。所以我想到的一个点是,在 namespace 修改完成后,还有没有资源属于宿主机,但是在容器内部可以访问到的。

回忆 Docker 历史漏洞:

以上两个漏洞都是因为外部资源由容器内部可控而引起的。

我们去了解一下这两个漏洞是如何修复的:

●官方修改了docker cp的流程,在 chroot 到容器之前,强制引用so库。

CVE-2019-5736介绍

为了更好的理解这个:CVE-2019-5736 我们不妨来看看这个漏洞当时是怎么利用的,它的原理是什么。这将为DirtyPipe的利用打下牢牢的基础。

利用过程

POC:

https://github.com/Frichetten/CVE-2019-5736-PoC

我们只看关键的payload

如果该poc被成功执行的话,会将宿主机的shadow文件拷贝至/tmp/目录下,并将其权限修改为777

于是我们尝试在容器中执行该POC文件:

image-20230108232044840

然后在宿主机执行 sudo docker exec -it 5a94 /bin/sh 会发现没有交互式的shell打开

image-20230108232112213

在Docker容器中发现POC被执行:

image-20230108232142304

查看宿主机的/tmp目录,发现存在权限777的shadow文件

image-20230108232214260

此时整个漏洞利用成功

原理分析
runC

runc是docker中最为核心的部分,容器的创建,运行,销毁等操作都是通过runc程序来完成的。我们查看runc文件如下:

image-20230108232433259

此时的runc结尾全部是0

当我们执行docker run等命令的时候实际上在底层调用的是runc程序,所以整个流程大概如下:

image-20230108232514900

其中虚线表示,当runc生成一个子进程后runc程序将会结束占用。

当触发poc后,runc程序会被重写并执行:

image-20230108232622371

/proc/

根据官方文档,/proc/文件夹类似于一个文件系统,其中存放着各个进程与本地文件之间的映射,其中:

/proc/[PID]/exe: 一种特殊的软连接,是该进程自身对应的本地文件

/proc/[PID]/fd/: 这个目录下存放了该进程打开的所有文件描述符

/proc/self/: 不同的进程访问该目录时获得的信息是不同的,内容等价于/proc/本进程pid/

/proc/[PID]/exe的特殊之处在于当权限通过的情况下打开这个文件,内核将会之间返回一个指向该文件的文件描述符,并非按照传统的打开方式做路径分析和文件查找,这就会导致绕过了mnt命名空间和chroot的限制。

mnt 命名空间

类似 chroot,将一个进程放到一个特定的目录执行。mnt 命名空间允许不同命名空间的进程看到的文件结构不同,这样每个命名空间 中的进程所看到的文件目录就被隔离开了。同 chroot 不同,每个命名空间中的容器在 /proc/mounts 的信息只包含所在命名空间的 mount point。

chroot

chroot - change root, 主要是将程序运行环境切换到指定目录。什么是将运行环境切换到指定目录呢,就是切换过去之后,在应用程序内,根目录“/”的位置变成了你指定的目录了,这样一来,就要求你的目录下必须包括程序的所有依赖环境,比如程序调用的系统命令(及其依赖项),系统调用的动态链接库等。

当执行docker exec命令的时候,runc启动并加入到容器的命名空间中去,其实这个时候,容器内的进程已经能够通过内部的/proc/观察到它,因此通过打开/proc/[runc-PID]/exe可以获取宿主机上的runc文件标识符,由此能够达到覆盖的能力。

#!

在unix中,凡是被#!注释的,统统是加载器的路径,常见的有#!/bin/sh,表示将该注释后的代码交给/bin/sh处理,我们常见的处理python脚本一般在bash中输入python run.py,而如果在run.py中将注释换成#!/path/to/python,则在bash中执行run.py即可。

在此poc中,作者将/bin/sh进行了覆盖,修改成了#!/proc/self/exe,意义在于当宿主机执行docker exec -it ID /bin/sh时,/bin/sh将会替换成执行调用者自己,也就是宿主机下的runc文件,此时将会执行runc文件。

两个for循环

在第一个for循环中,攻击者持续监测/proc/目录,当产生runc进程时,以可读的方式打开runc文件夹获取文件标识符。

在第二个for循环中,攻击者持续监听等待runc程序结束占用后就能够用之前循环获得的文件标识符以可写的方式向runc文件内写入payload。

流程图

image-20230109115352550

修复

对于 CVE-2019-5736 的修复,在这里详细地去介绍一下,因为这是 DirtyPipe 引发 Docker 逃逸的关键。

搜索runc的git commit记录 (https://github.com/opencontainers/runc/commit/6635b4f0c6af3810594d2770f662f34ddc15b40d)。

image-20230108201323143

添加了ensure_cloned_binary的判断,从字面意思看是确保是复制的二进制文件,跟进这个函数。

image-20230108201332427

从字面意思看,会判断是否为自身的复制,如果不是,则对自身进行复制,然后运行复制文件,关闭当前文件,跟进看怎样进行的复制。

image-20230108201357072

调用了memfd_create函数,该函数意思为在内存中创建一个匿名文件,也就是说runC将自身复制为一个匿名文件,然后在容器中运行,从而保证了容器内部无法获取runC文件本身。

那此处的runC已经被修改为匿名文件了,还有没有什么方式去获取真正的runC呢?

看到利用以及修复,这时候很多师傅应该已经可以想到,使用runC去执行/proc/self/exe时,其符号链接指向的就是runC本身。

然而,由于 Linux Namespace 机制,即使指向runC本身,内部的修改也可以看作一次copy,并不会对外部文件产生影响。但是,DirtyPipe突破了这层壁垒。

DirtyPipe 这个洞的利用过程与 Linux 管道和 splice(2) 系统调用的实现机制有关,其对文件的操作由内核来操作,对于runC漏洞的修复,由于容器内部namespace的转换,文件的符号链接即使指向外部,也无法对外部的文件产生影响,而从内核层的操作便跨越了namespace对文件的影响,从而可以对外部的runC进行覆盖。

分析完利用链,接下来去看如何去利用。

我们需要去做的是知道DirtyPipe这个洞的利用条件以及能做什么。

漏洞发现者在自己发表的文章(https://dirtypipe.cm4all.com/) 中讲述得很清晰。

  1. 该漏洞是作用于缓存机制上的,所以缓存刷新即设备重启后被覆盖的文件会恢复。

  2. 该漏洞只能覆盖一个可读文件。

  3. 该漏洞不能跨页覆盖,作者原 POC 给出的 Page-Size 为 4096 字节。

  4. 该漏洞不能在页头或者页尾开始覆盖,即无法覆盖第一个字符。

刚好,我们现在得到了一个符号链接指向外部的runC,由于 magic links 的特殊性(会直接调用专属的处理函数并返回对应文件的文件描述符),通过 DirtyPipe 跨越 namespace 的限制覆盖此符号链接,就可以对宿主机的runC进行覆盖了。相对于CVE-2019-5736的利用,DirtyPipe 的覆盖比较麻烦,因为无法覆盖第一个字符,所以需要去寻找函数地址,通过写 shellcode 的方式进行利用。不过这相对来说就比较轻松了。

附件中给出的poc其实就是在完成这件事:

循环获取runc的进程,获取runc入口点偏移,利用dirtypipe写入runc 入口点shellcode