CVE-2024-2961漏洞分析
本周Linux GLIBC的库函数iconv缓冲区溢出漏洞(CVE-2024-2961)的细节/PoC[1]被公开,目前已知的利用方式是可以让PHP的任意文件读取漏洞升级的远程命令执行漏洞。本文将对公开的漏洞细节和PHP利用思路进行分析研究。
ICONV漏洞详情
CVE-2024-2961本质上是GLIBC中iconv库的漏洞,我认为该漏洞的发现巧合性很大,该漏洞的发现者是通过fuzz php发现该漏洞的,如果单纯的fuzz iconv库是没办法导致crash,就算是fuzz php,一般情况下就算触发了该漏洞也很难导致crash。
首先是漏洞点,位于glibc/iconvdata/iso-2022-cn-ext.c
文件,相关代码如下所示:
1 | else if ((used & SS2_mask) != 0 && (ann & SS2_ann) != (used << 8))\ |
在上面代码中的这两个分支,将会把输入转译4字节的输出,并且不会检查输出buf的长度。可能产生6种输出:
1 | \x1b$*H 0x1b 0x24 0x2A 0x48 |
再来看看PoC,代码如下所示:
1 | /* |
编译上面的代码运行:
1 | $ gcc poc.c -o poc |
我们使用python来看看poc中的特殊字符:
1 | BUG = "劄".encode() |
从上面的结果可以看出,这个特殊字符只占3字节,但是却会被转译为\x1b$*H
四字节,产生了一字节的溢出,上面的poc似乎还是不太好展示出该漏洞的影响情况,我们可以简单的改改代码,如下所示:
1 | void main() |
从上面的结果可以看出,我们成功的溢出了1字节到overflow
变量中。
php任意文件读到RCE
在了解完ICONV漏洞原理之后,接下来再看看该漏洞的实际利用场景。目前已公开的漏洞利用场景只有一个,就是把php的任意文件读取漏洞转换为远程命令指令漏洞。
我们首先来看看以下PHP代码:
1 |
|
CTF的web手应该都知道,我们可以构造PoC:php://filter/read=convert.iconv.UTF-8.ISO-2022-CN-EXT/resource=data:text/plain;base64,xxxxxxx
。
这样我们就可以调用到iconv_open("ISO-2022-CN-EXT", "UTF-8");
,接着控制iconv
函数的输入buffer,达到触发iconv漏洞的目的。
环境搭建
首先我们需要搭建一个测试环境,Dockerfile如下所示:
1 | $ cat Dockerfile |
环境搭建好以后,我们可以直接使用公开的PoC进行漏洞利用,能成功执行任意命令,该过程就不再赘述。
PoC分析
首先我们来看看公开的python PoC脚本[2],该PoC可以分为3个步骤。
- 首先,对目标是否能进行漏洞利用进行检测,该检测过程没法检测目标是否存在漏洞,只能检测目标是否存在进行漏洞利用的条件,有以下三个方面:
- 检测目标的任意文件读是否支持:
data:text/plain;base64,
。 - 检测目标的任意文件读是否支持:
php://filter//resource=data:text/plain;base64,
。 - 检测目标的任意文件读是否支持:
php://filter/zlib.inflate/resource=data:text/plain;base64,
。
- 通过
/proc/self/maps
获取目标的内存布局,获取目标libc文件。获取目标内存布局需要获取libc的基地址,php堆的基地址。libc的基地址很好获取,但是php堆的基地址就得猜测,没办法100%确定,php堆有以下条件:
- 大小在
0x200000
之上,并且为该大小的倍数,所以还需要0x200000对齐。 - 该内存段不属于任何二进制文件。
- 该内存段的权限为:
rw-p
- 构造Payload,发送Payload到目标进行漏洞利用。
漏洞利用分析
接下来分析该PoC中是如何构造Payload进行漏洞利用的。
调试环境搭建
我们先来搭建一个漏洞调试环境,步骤如下:
1 | # 安装apt-src |
利用分析
简单分析一下PoC可以得知,该漏洞利用的思路在CTF中算是简单题,程序复杂度上比CTF的难。
如果把这道题看成CTF,那么就是一个在已知内存地址,libc的情况下进行堆的漏洞利用。并且php的堆分配并不是直接使用libc的malloc,而且封装了自己的堆函数。
所以我们需要关注php的堆管理,首先需要关注_zend_mm_heap
结构体:
1 | struct _zend_mm_heap { |
在该结构体中,我们需要关注free_slot
,这个结构体可以等同于最古老的tcache
,因为没有任何的检查,利用难度直线下降。
如果是在一个CTF题目中,我们可以用以下利用思路:
- 分配x个相同大小并且地址连续的堆,然后释放它们,那么它们会被放入tcache中形成链表。
- 我们获取第一个堆,并且通过漏洞溢出1字节,这样将会覆盖下一个堆的tcache链表指针。
- 因为溢出的一字节不可控,在此例中,为0x48,所以我们需要该地址的堆可以让我们任意地址写入。并且在之前控制该地址的值指向我们想要控制的任意地址,比如
free_hook
地址,这样我们之后分配的堆就能获取到free_hook
地址的堆,达到控制free_hook
的目的,从而RCE。
在构思完思路后,我们来具体模拟一下:
1 | 1. 有三个大小为0x100的连续的堆 |
以上为CTF中的利用思路,但是CTF中PWN题目的程序复杂度比较低,考验的都是漏洞利用技巧,很少会考验逆向能力,所以可以很容易控制堆分配和堆释放。但是在实际利用中,程序的复杂度不是一个量级的。
在当前漏洞中,我们的测试环境中,php只会调用file_get_contents
函数,我们也只能控制该函数的参数,我们并不能很明显的控制malloc/free
函数,这就需要我们对file_get_contents
函数进行逆向分析,看看在php源码中,我们如果控制file_get_contents
函数调用堆分配/释放,并且获取我们需要大小的堆。
经过对公开的PoC进行调试,结合分析php的源码,我们可以得知以下几点:
zlib.inflate
的作用是进行zlib解压缩,将会调用php的php_zlib_inflate_filter
函数,并且在php_zlib_filter_create
函数中限制了能分配的最大堆尺寸为0x8000。dechunk
的作用是处理HTTP CHUNKED,将会调用php的php_chunked_filter
函数,我们可以通过该函数,buffer的size标志位缩减到任意值。没法控制堆的大小,只能控制有效长度的标志位。在file_get_contents
函数的流程中,用户输入的buffer都是放在php_stream_bucket
结构体中,该结构体的定义如下:
1 | struct _php_stream_bucket { |
在该结构体中,buf指向一个堆缓冲区,比如指向一个大小为0x8000的堆,但是buflen
表示的是数据的有效长度,比如可以是0x8000,那么该堆中的数据都是有效的,通过dechunk
过滤器,我们可以缩减buflen
的长度为任意值,比如缩减到0x100,那么堆还是0x8000的堆,但是只有前0x100字节的数据是有效数据。
convert.quoted-printable-decode
的作用是对=00
格式的数据进行解码,变为\x00
。convert.iconv.x.x
的作用调用iconv
函数对数据进行编码转换。在PoC中使用两种:convert.iconv.UTF-8.ISO-2022-CN-EXT
和convert.iconv.latin1.latin1
。
其中convert.iconv.UTF-8.ISO-2022-CN-EXT
很明显是用来触发漏洞的。但是convert.iconv.latin1.latin1
的作用需要仔细分析。
convert.iconv.x.x
过滤器调用的是php_iconv_stream_filter_do_filter
函数,进过分析发现,在该函数中输出的buffer会根据buflen
对堆进行重新分配。比如输出的buffer是一个0x8000的堆,但是buflen=0x100
,那么会根据该长度申请一个新的堆,作为iconv
的输出,那么经过iconv
编码转换,因为输入输出的编码一样,所以输出数据不变,只变化了堆的大小。
通过上面的分析可以发现,在PoC中dechunk
和convert.iconv.latin1.latin1
都是组合出现的原因,因为这样可以控制获取任意大小的堆。通过dechunk
把buflen
设置为0x8000
以下的任意值,然后使用convert.iconv.latin1.latin1
把堆修改为相应的size。除了可以分配任意size的堆,还可以把任意size的堆放入free_slot
中。
PoC中利用的目标为修改_zend_mm_heap
结构体中的custom_heap
结构,作用和free_hook
类似,因为在emalloc
中有以下代码:
1 | ZEND_API void* ZEND_FASTCALL _emalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) |
把custom_heap
结构体中的_free
设置为system
,那么调用efree
的时候就能执行system函数了。
漏洞调试
接下来通过调试的方式加学习在php的实际环境中如果构造利用链。
建议在以下位置下断点调试,能比较清晰的看出堆的变化情况:
1 | ► 0x5555557dfae5 <_php_stream_fill_read_buffer+309> call qword ptr [rax] <php_zlib_inflate_filter> |
我们可以直接把这个断点加入到gdbinit
中:
1 | $ cat .gdbinit |
由于我们使用gdb调试,而gdb默认会关闭地址随机化,所以我们可以在gdbinit
中定义一个指令方便我们查看php的堆信息,还可以再添加一个指令,方便我们查看pbucket的情况,最终的gdbinit
内容如下所示:
1 | $ cat .gdbinit |
接下来还需要编写一个python脚本,方便我们控制生成payload,如下所示,有些函数直接参考了公开的PoC:
1 | #!/usr/bin/env python3 |
以上就是我通过公开的PoC修改的一版调试用的python脚本,我们只需关注该脚本中的buildPayload
函数就好了。调试命令也很简单:python3 poc.py && gdb ./php8.1-8.1.2/fpm-build/sapi/cli/php
。
接下来我们看看怎么跟CTF的思路关联起来,在CTF中比较容易利用的原因是我们能很容易的控制堆分配和释放,所以现在我们来看看我们需要如何在php中控制堆的分配和释放。
跟公开的PoC一样,我们选择控制长度为0x100的堆(这个长度的堆比较好对齐)。
在此之前,我们还要知道在php堆的free_slot
中,堆的尺寸是如何分布的,可以参见zend_alloc_sizes.h
文件,如下所示:
1 | /* num, size, count, pages */ |
所以我们要查看0x100大小的堆在free_slot
中的情况,可以使用以下命令:
1 | pwndbg> php_heap |
获取一个0x100大小的堆
要让php分配一个0x100大小的堆,buildPayload
函数的编写可以参见以下代码:
1 | def buildPayload() -> str: |
通过调试查看堆分配情况,过程如下所示:
第一次断点断在php_zlib_inflate_filter
函数,该函数将会对输入的数据进行zlib解压缩,gdb情况如下所示。
1 | ► 0x5555557dfae5 <_php_stream_fill_read_buffer+309> call qword ptr [rax] <php_zlib_inflate_filter> |
第二次断点断在php_chunked_filter
函数,并且查看输入的bucket
结构,里面的内容为输入的0x8000长度的数据,gdb详情如下所示:
1 | pwndbg> c |
第三次断点断在了php_iconv_stream_filter_do_filter
函数,然后查看bucket
内容,发现buf的堆地址没变,只有buflen
被修改为了0x100,gdb详情如下所示:
1 | pwndbg> c |
最后一步不能再执行continue了,因为程序会运行结束,我们需要使用next指令观察执行完php_iconv_stream_filter_do_filter
函数以后bucket的情况,gdb过程如下所示:
1 | pwndbg> ni |
从上面可以看出我们成功的申请到了一个0x100长度大小的堆。
释放一个长度为0x100大小的堆
buildPayload
函数的编写可以参考以下代码:
1 | def buildPayload() -> str: |
前面四步和上面一样,我们从第二个dechunk
执行完开始,gdb过程如下所示:
1 | pwndbg> p *brig_inp.head |
第二个dechunk
执行完以后,buf仍然是长度为0x100的堆,但是buflen
被修改为了0x10。接着我们把程序停在执行完php_iconv_stream_filter_do_filter
函数之后,再查看堆信息,gdb过程如下所示:
1 | pwndbg> p *brig_outp.head |
从上面的结果可以看出,大小为0x100的堆(0x7ffff5288100
)已经被释放并且被放入free_slot
当中。
触发漏洞
完成了上面两步的调试过程,我们已经可以像做一道CTF的堆题一样,随意的控制malloc
和free
。
现在我们来尝试按照上面分析CTF题的步骤来构造触发漏洞的利用链。
经过一番调试分析,我构造的利用链步骤如下:
- 最开始0x100大小的堆的free链表为:
0x7ffff5288100->0x200->0x300->0x400->0x500...
。 - 申请三个堆后,free链表为:
0x7ffff5288400->0x500->0x600...
。 - 把这三个堆释放后,free链表为:
0x7ffff5288300->0x200->0x100->0x400...
。 - 再次申请两个堆,地址为
0x7ffff5288300
,0x7ffff5288200
。 - 把这两个堆释放,这个时候free链表为:
0x7ffff5288200->0x300->0x100->0x400...
- 触发漏洞,这个时候
0x7ffff5288200
会被用来存放iconv
的结果,所以能溢出1字节覆盖到了0x7ffff5288300
地址的第一字节,这个时候free链表变为了:0x7ffff5288300->0x148->...
。 - 由于触发漏洞时,iconv返回-1,所以
0x7ffff5288200
堆在溢出后会被释放,这个时候free
链表为:0x7ffff5288200->0x300->0x148...
。
根据上面的步骤,来编写buildPayload
函数,代码如下所示:
1 | def buildPayload() -> str: |
接着使用gdb调试查看堆布局,如下所示:
1 | $ python3 poc1.py && gdb ./php8.1-8.1.2/fpm-build/sapi/cli/php |
从上面的内存布局可以看出,程序已经按照我们的设想触发漏洞,溢出覆盖了free_slots
的指针。
最终利用
最终的利用思路我们参考了公开的PoC中的利用思路,控制_zend_mm_heap
结构体中的custom_heap
,该利用思路有个前置条件,需要设置_zend_mm_heap->use_custom_heap
为非0值。并且我们不能只修改custom_heap._free
,还同时需要设置custom_heap._malloc
和custom_heap._realloc
,因为当_zend_mm_heap->use_custom_heap
非0时,这三个函数皆会调用其custom
函数。
在前面利用思路的基础上,我们的利用链要修改/新增以下步骤(这里需要注意,libc地址和php的_zend_mm_heap
地址都为已知信息。):
- 因为
0x7ffff528b300
指向了0x7ffff528b148
,所以我们需要控制该地址,恰好0x7ffff528b100
是第一步中申请到的第一个堆。所以我们需要让step1_malloc_step2_free
指向_zend_mm_heap
段的地址。 _zend_mm_heap
的地址为0x7ffff5200040
,我们利用的堆的大小为0x100,从0x7ffff5200050
开始,0x100的大小,可以覆盖到所有的free_slot
。所以,我们让0x7ffff528b148
指向0x7ffff5200050
。- 我们需要申请三个堆,把
0x200->0x300->0x148
这三个堆分配出来。这个时候free链表头为:0x7ffff5200050
。 - 申请一个堆,这个堆的地址为:
0x7ffff5200050
,写入我们需要控制的值。首先把size位设置为0x200000,free_slot只设置0x140和0x18的地址,其他皆为0。0x140的堆指向0x7ffff5200040
,用来设置use_custom_heap
,0x18的堆指向0x7ffff5200040 + 0x168
,用来设置custom_heap
。这里为什么设置0x140的堆呢?这个值是可以变化的,在这里参考了公开PoC中的定义cmd的命令长度为0x140,如果命令长度不够,则用\0
填充到0x140的长度。 - 写入
use_custom_heap
和custom_heap
的值。 - 写入需要执行的命令字符串,当该堆释放的时候,就会调用system执行指定命令。
这里需要注意,执行的命令建议加上kill -9 $PPID;
,否则所有堆里的数据都会被当成命令去执行一遍。
根据以上思路,编写buildPayload
函数,代码如下所示:
1 | def chunked_add_bad_data(data: bytes, badData: bytes, totalsize: int)->bytes: |
注意,因为提供的地址都是使用gdb调试的时候地址,因此上面的Payload只能在调试状态下成功执行命令。
总结
经过一番自己的调试分析,发现公开的PoC已经非常完善了,利用链没法优化的更好,并且进行了两次zlib压缩,能把payload压缩到非常短的地步。
虽然目前公开的只有对PHP进行利用的PoC,但是iconv漏洞的影响面还是非常广的,后续将继续对iconv的使用面进行研究,是否还有其他应用受该漏洞的影响。
参考链接
CVE-2024-2961漏洞分析