0x00 前言
前几天p牛师傅在星球发了一个帖子:PHP利用glibc iconv()中的一个缓冲区溢出漏洞CVE-2024-2961,实现将文件读取提升为任意命令执行漏洞,当时觉得这个漏洞蛮有意思,就想研究一下。于是web狗开启了一次二进制漏洞的学习之旅。
在nvd网站上是这样描述的:
谷歌翻译过来就是:GNU C 库 (也就是glibc)2.39 及更早版本中的 iconv() 函数在将字符串转换为 ISO-2022-CN-EXT 字符集时,可能会使传递给它的输出缓冲区溢出最多 4 个字节,这可能会导致应用程序崩溃或覆盖相邻变量。
缓冲区溢出是二进制安全研究领域里很常见的漏洞。所谓缓冲区溢出是指当一段程序尝试把更多的数据放入一个缓冲区,数据超出了缓冲区本身的容量,导致数据溢出到被分配空间之外的内存空间,使得溢出的数据覆盖了其他内存空间的数据。攻击者可以利用缓冲区溢出修改计算机的内存,破坏或控制程序的执行,导致数据损坏、程序崩溃,甚至是恶意代码的执行。缓冲区溢出攻击又分为栈溢出、堆溢出、格式字符串溢出、整数溢出、Unicode溢出。(参考:https://info.support.huawei.com/info-finder/encyclopedia/zh/缓冲区溢出.html)
iconv()函数是Glibc提供的用于字符编码转换的API,可以将输入转换成另一种指定的编码输出。比如将原本为gbk编码的输入转化为utf-8的编码输出。作者发现当将“劄”、“䂚”、“峛”或“湿”等采用utf-8编码的汉语生僻字(博大精深的汉字)转化为ISO-2022-CN-EXT字符集输出时,会导致输出缓冲区有1-3字节的溢出。具体的代码分析见文末原作者的博客链接。
可以使用原作者提供的POC来(https://raw.githubusercontent.com/ambionics/cnext-exploits/main/poc.c)复现下这个漏洞:
gcc -o poc ./poc.c && ./poc
作者检查了glibc团队的提交历史记录,发现这个漏洞出现在 2000 年,已经有 24 年的历史了!!!
那么,这个缓冲区溢出漏洞又该怎样利用呢?
作者一番艰辛的寻找谁调用iconv()
,始终未能找到合适的目标,最终回到PHP上,却发现这个缓冲区溢出漏洞居然在PHP应用程序中大放异彩~
相信每个搞安全的师傅对php伪协议php://filter
一定不陌生,php://filter
也叫php过滤器,我们经常使用convert.base64-encode
这个过滤器来进行文件读取
php://filter/convert.base64-encode/resource=...
原作者发现在php过滤器中有一个叫convert.iconv.X.Y
的过滤器也是将字符集从 X 转换为 Y,后来证实在linux上其底层就是使用了glibc的iconv()
!!!接下来的0x03和0x04是本文最重点也是最难懂的部分,因为涉及到了php底层实现上的东西(当然也省略掉了一些细节)。对利用原理不是那么想了解的师傅可以直接跳到0x05进行RCE复现。下面的内容其实也主要是对原作者博客的翻译以及自己的理解:
我们知道,程序要运行就得在内存申请一块空间来存放数据、函数调用栈等。在php中是使用emalloc(N)
函数来分配内存的,N是希望分配的字节数,返回的结果是一个指向能够存储至少N字节的内存块(即chunk)的指针。当完成对这块内存的使用后,通过调用efree(ptr)
来释放它。PHP中的内存块有多种大小:8(2^3) 0x10(2^4), 0x18(2^5), ... 0x200(2^9)...
PHP堆由一个2MB(2^21)大小的区域组成,被分割成512(2^9)个页面,每个页面大小为0x1000(2^12)字节。每个页面上可能包含特定大小的chunks。各chunk之间没有元数据信息存在。
PHP为每种大小的块(chunk)都维护了一个空闲列表,其数据结构是一个单链表,且遵循后进先出(LIFO)的原则。空闲列表是这样建立的:每个空闲块的前8个字节内包含指向下一个同样大小空闲块的指针,指针也可理解为下一个同样大小空闲块的地址。
当释放某个大小的块时,该块的前8个字节存放此前空闲列表头部元素的地址,该块变为空闲列表的头部元素;当需要分配N字节的空间时,PHP会查找对应大小的空闲列表,取出头部元素并返回(能够存储至少N字节的内存块的地址)。如果空闲列表是空的(即所有可用的块都已被分配), PHP会查看堆元数据以找到未使用的页面。然后在该页面上创建空白块并将其放入空闲列表中。PHP堆的可视化表示如下图所示:
一个PHP堆包含512个页面,这里第5页存储大小为0x400(2^10)的块。它包含4个块(因为4 × 0x400 = 0x1000,即一页的大小),块#1和#3已被分配,而块#2和#4已被释放。因此,它们处于大小为0x400的块的空闲列表中。块#2中有一个指向0x7ff10201400 的指针,那是大小为0x400 的下一个空闲块(即块#4)的地址。结合CVE-2024-2961假如我们能从 块 #1 溢出到 块 #2, 则会覆盖掉这个指针。即使只有一个字节溢出, 也能改变空闲列表指针, 从而更改空闲列表。
但PHP对每个HTTP请求会创建新堆,我们要怎样才能知道该在内存的什么地方进行溢出呢?这是进行利用的一个难点。原作者将在系列文章的第二部分进行介绍。
PHP在处理过滤器时,首先会获取流(读取资源)。流是存储在一系列bucket中的,这些bucket是双向链接的结构,每个bucket包含一定大小的缓冲区。以读取/etc/passwd
为例,可能会有3个bucket:第一个可能包含文件的前5个字节,第二个bucket再增加30个字节,第三个bucket则再增加1000个字节。它们连接在一起构成了一个bucket传送带系统。
获取流之后就是应用过滤器对流进行处理了。处理过程是这样的:取第一个过滤器并对第一个bucket进行处理。为此,会分配一个与bucke缓冲区大小相同的输出缓冲区(例子中是5个字节)并进行转换。例如,如果过滤器是string.upper
,它会将输入缓冲区中的每个小写字符转换为其在输出缓冲区中的大写等价物并创建一个新的指向这个输出缓冲区的bucket,接着继续处理第二个bucket,第三个bucket,直到最后一个bucket,每个输出bucket又形成了一个新的传送带序列:
最后在这个序列上继续应用第二个过滤器,第三个过滤器,直到处理完最后一个过滤器。
假如有一个网站可以利用php://filter/convert.base64-encode/resource=...
来进行任意文件读取,现在知道了可以利用convert.iconv.XXX.ISO-2022-CN-EXT
进行缓冲区溢出,那么要怎样才能远程代码执行呢?
利用缓冲区溢出攻击需要先绕过安全机制ASLR(地址空间布局随机化)和PIE(位置无关可执行文件),然而有了任意文件读取这个前提可以通过读取/proc/self/maps
来得到内存代码段基址进行绕过。当然,也可以利用任意文件读取来获取libc库版本来检查其是否已打补丁。获得代码执行的思路就是利用单字节缓冲区溢出来修改指向空闲块指针的最低有效位(LSB),以便获得对某些空闲列表的控制权,从而写入命令到内存执行,实现RCE。
这里涉及到pwn的概念了,参考链接:
ASLR和PIE:
https://blog.csdn.net/weixin_62675330/article/details/122907754
http://wiki.allinsec.cn/?p=306
/proc/self/maps:
https://www.jianshu.com/p/3fba2e5b1e17
https://blog.csdn.net/qq_52630607/article/details/128347260
前面说到了php处理过滤器时的bucket队列技术,然而实际上无论是读取文件还是请求HTTP URL,亦或是使用ftp://
协议,PHP都只生成包含整个响应内容的一个bucket。无法利用单一的bucket来填充堆或操作修改后的空闲列表。
这是为什么呢?因为借助单个bucket,我们可以溢出到一个空闲块并修改空闲列表,但随后我们就用完了所有的bucket,而要利用已修改的空闲列表进行操作至少还需要再分配两个bucket!(为什么至少再需要两个,看到下面空闲列表控制就能明白了)
作者发现可以用一个叫zlib.inflate
的过滤器来解决这个问题。该过滤器接收流并对其进行解压缩处理。为此,它会分配一个大小为8页(0x8000字节)的缓冲区并将流填充至其中。如果这个缓冲区不足以容纳全部数据, 它将再新创建一个相同大小的缓冲区来存储剩余部分;若前两个缓冲区仍不够用, 则继续增加更多的缓冲区。然后将每个缓冲区都添加到bucket中。可以使用此过滤器创建任意数量的bucket:
然而,这些bucket的缓冲区大小为0x8000,这个大小并不利于利用;这种大小的缓冲区分配方式与上面描述的不同,并且在释放后不会进入空闲列表。因此需要调整存储bucket的大小。
可以利用过滤器dechunk,该过滤器用于解码经过HTTP-chunked编码的字符串。
HTTP-chunked编码通过数据块(非堆内存块)发送数据。它先发送一个以十六进制表示的大小,紧接着是一个换行符,然后是相应大小的数据块,再接一个换行符。接着发送另一个大小、另一个数据块、再一个大小、又一个数据块,并通过发送大小为0(零)来指示数据的结束,如图示:
解块后结果是:This is how the chunked encoding works
那么为了得到任意大小的bucket,我们应在每个bucket的前面先填充成千上万个零,然后提供一个大小和数据块,如下所示:
目标是通过将某些指针的最低有效位(LSB)覆盖为值0x48(ASCII中的H)来修改某个空闲列表。为了能无条件地达到相同效果,针对大小为0x100的块,因为这些块地址的最低有效位总是零。这意味着我们的溢出效果始终相同:给一个块指针增加0x48。
通过下面六个步骤来修改指针达到控制空闲列表的目的:
为了便于描述将大小为0x100的块的空闲列表命名为FL[0x100],假设已经通过分配大量0x100大小的块成功填充了堆。因此,在内存中的某个位置,必有三个连续的空闲块A、B和C,其中A是FL[100]的头。A指向B,B指向C(步骤1)。我们可以分配这三个块(步骤2),然后再次释放它们(步骤3)。此时,空闲列表被反转:我们得到的是C→B→A。接着我们再次进行分配,但这次我们在C的偏移量0x48处放置了一个任意指针0x1122334455(步骤4)。再次释放它们(步骤5)后,状态与步骤1完全相同,但有一个小差异:在C+0x48处存在一个任意指针。现在我们可以从块A执行溢出操作,这将改变B中包含的指针的位置。它现在指向了C+0x48的位置,结果使得空闲列表变为 B→ C+0x48 → 0x1122334455。再进行 3 次分配,就可以让 PHP 分配在我们的任意地址。于是拥有了一个“写入任意位置”的功能。
回到漏洞利用的实现上来,在此处描述的各个步骤中,分配块然后释放块。但我们无法真正摆脱bucket:我们只能改变它们的大小。然而,我们只关心大小为0x100的块—好像其他块不存在一样!因此将每个bucket构建为如下HTTP 分块:
对于漏洞利用的每个步骤,都会调用 dechunk 过滤器:因此每个 bucket 的大小都会发生变化。有些 bucket 的大小变为 0x100,因此在漏洞利用中“出现”,而有些 bucket 则变小,因此消失。这为我们提供了一种完美的方法,可以让 bucket 在特定时刻实现,并在不再需要它们时将其丢弃。
虽然可以通过读取 /proc/self/maps 来查看内存区域,但我们并不清楚自己在堆中的确切位置。可以通过定位PHP的堆来完全忽略这个问题。由于它的对齐方式(~0x1fffff)和大小(2MB),它很容易识别。它的顶部有一个 zend_mm_heap 结构,其中包含非常有用的字段:
struct _zend_mm_heap {
...
int use_custom_heap;
...
zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
...
union {
struct {
void *(*_malloc)(size_t);
void (*_free)(void*);
void *(*_realloc)(void*, size_t);
} std;
} custom_heap;
};
首先,它包含每个空闲列表。通过覆盖空闲列表,可以获得任意数量、任意大小的写入内容来覆盖最后一个字段 custom_heap,其中包含 emalloc()、efree() 和 erealloc() 的替代函数(类似于 glibc 中的 _malloc_hook 及其同类函数)。然后将 use_custom_heap 设置为 1,并在 bucket 上调用 free(),从而获得带有受控参数的任意函数调用。由于可以使用文件读取来访问二进制文件,因此可以构建花哨的 ROP 链,但为了尽可能通用,故将 custom_heap.free 设置为system(),允许我们以 CTF 方式运行任意 bash 命令。
利用脚本执行了三个请求:首先下载/proc/self/maps
文件,并从中提取PHP堆的地址和libc库的文件名。接着下载libc二进制文件来提取system()
函数的地址。最后执行一次最终请求来触发溢出并执行预设的任意命令。漏洞利用脚本地址:
https://raw.githubusercontent.com/ambionics/cnext-exploits/main/cnext-exploit.py
首先在我的ubuntu虚拟机上安装docker:
1、配置国内源
sudo vim /etc/apt/sources.list
deb https://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb-src https://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse
deb-src https://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
deb-src https://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
# deb https://mirrors.aliyun.com/ubuntu/ jammy-proposed main restricted universe multiverse
# deb-src https://mirrors.aliyun.com/ubuntu/ jammy-proposed main restricted universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse
deb-src https://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse
2.Add Docker's official GPG key
udo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
3.Add the repository to Apt sources
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://mirrors.aliyun.com/docker-ce/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
4.安装新版docker
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
5.查看docker状态,设置开机自启动
sudo systemctl status docker
sudo systemctl enable docker
然后,下载vulhub运行环境,p牛师傅已经做好了环境:
git clone https://github.com/vulhub/vulhub.git
cd vulhub/php/CVE-2024-2961/
sudo docker compose up -d
访问http://ip:8080,通过post传参file就可以任意文件读取:
下载原作者的exp并安装相关依赖(需要Linux和Python 3.10解释器)
wget https://raw.githubusercontent.com/ambionics/cnext-exploits/main/cnext-exploit.py
pip3 install pwntools
pip3 install https://github.com/cfreal/ten/archive/refs/heads/main.zip
运行exp反弹shell:
1.据原作者描述该漏洞影响PHP 7.0.0 (2015) 到 8.3.7 (2024)近十年php版本的任何php应用程序(Wordpress、Laravel 等)。PHP的所有标准文件读取操作都受到了影响:file_get_contents()、file()、readfile()、fgets()、getimagesize()、SplFileObject->read()等。文件写入操作同样受到影响(如file_put_contents()及其同类函数)。
2.关于该漏洞的其他利用场景:原作者提出了PHP-MySQL注入到RCE,XXE到RCE,phar的替代品,new $_GET['cls']($_GET['argument']);
,文件读取反序列化(unserialize()
)也可以利用CVE-2024-2961这个漏洞将其升级为远程代码执行。总之,只要能控制文件读取或写入端点的前缀,就可能实现远程代码执行(RCE),具体可以看原作者博客。国内师傅还提出了可以用来绕过disable_functions。
3.一个存在24年的缓冲区漏洞,稳定影响了近十年的PHP版本,任何PHP程序,可以说是通杀了。
4.原作者的研究成果会分为三部分来讲述,目前只发表了第一部分内容。第二部分将更深入地研究PHP引擎,在非常流行的PHP Webmail中找到iconv()调用;第三部分将讨论盲文件读取的利用方法。让人期待,不得不佩服国外安全研究员的钻研精神,为一个二进制中常见的缓冲区溢出漏洞如此去寻找利用场景。
5.这是一个二进制漏洞的web利用,涉及到较深的二进制和pwn了,这块完全是我的盲区了,所以其实对于将文件读取提升为RCE这个过程的原理和整套流程我还只是一知半解,相信随着自己日复一日对各种漏洞原理的学习与钻研,终有一天会豁然开朗。
最后,附上本文的一些参考链接:
原作者博客:https://www.ambionics.io/blog/iconv-cve-2024-2961-p1
国内师傅的翻译:https://xz.aliyun.com/t/14690、https://mp.weixin.qq.com/s/03Bq8iryo22Cme5QR0LqDw(翻译都多少会有点问题,建议结合原文来看)
p牛师傅的环境:https://github.com/vulhub/vulhub/tree/master/php/CVE-2024-2961
原作者公开exp:https://github.com/ambionics/cnext-exploits
珂技知识分享:https://mp.weixin.qq.com/s/zcokg-eNjkNpxJZwFm0Zyg
如果喜欢小编的文章,记得多多转发,点赞+关注支持一下哦~,您的点赞和支持是我最大的动力~