php伪协议之php://

在红帽杯的比赛里面,文件包含漏洞的利用使用了php://filter去获取后台文件的源码,这里就写一下php://filter的一些使用
php://filter在文件包含漏洞的利用中经常会用到,PHP提供了一些杂项输入/输出(IO)流,允许访问PHP的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。有php://stdin, php://stdout , php://stderr,php://input,php://output等等。
先提一下,php://input可以获取到POST过来的数据,这是我觉得挺有趣的一个地方。

1
2
3
<?php
readfile("php://input");#readfile用于读取信息流或文件,并写入输出缓冲
?>

image

巧用编码和解码

然后,来看php://filter,它有很多妙用。先写一个test.php文件,内容如下

1
2
3
<?php
include("shell.php");
?>

我在该文件的目录下有一个叫shell.php的文件,代码如下

1
2
3
4
<?php 
$username=admin;
$password=123456;
echo "go back!!"?>

那么,此时访问localhost中的test.php,结果如下image但是,如果把test.php中的代码改为

1
2
3
<?php
include("php://filter/convert.base64-encode/resource=shell.php");
?>

这时候再访问localhostimageimage可以看到,获取到了shell.php的源码,在红帽杯中的biubiubiu那题,也对一个文件包含漏洞应用了这个,详细的可以看我写的红帽杯writeup那篇。然后,有的ctf题中会有这样的代码

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

$content在开头增加了exit过程,导致即使我们成功写入一句话,也执行不了(这个过程在实战中十分常见,通常出现在缓存、配置文件等等地方,不允许用户直接访问的文件,都会被加上if(!defined(xxx))exit;之类的限制)。那么这种情况下,如何绕过这个“死亡exit”?
幸运的是,这里的$_POST['filename']是可以控制协议的,我们即可使用 php://filter协议来施展魔法:使用php://filter流的base64-decode方法,将$content解码,利用php base64_decode函数特性去除“死亡exit”。
众所周知,base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。
所以,一个正常的base64_decode实际上可以理解为如下两个步骤:

1
2
3
<?php
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);

所以,当$content被加上了<?php exit;?>以后,我们可以使用php://filter/write=convert.base64-decode来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和我们传入的其他字符。
“phpexit”一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个“a”一共8个字符。这样,”phpexita”被正常解码,而后面我们传入的webshell的base64内容也被正常解码。结果就是<?php exit;?>没有了。我把test.php改成了上面那段代码,然后POST数据:image然后打开shell.php,发现代码已经修改成功,太强了!image

利用字符串操作方法

其实,除了使用base64特性的方法外,我们还可以利用php://filter字符串处理方法来去除“死亡exit”。我们观察一下,这个<?php exit; ?>实际上是什么?
实际上是一个XML标签,既然是XML标签,我们就可以利用strip_tags函数去除它,而php://filter刚好是支持这个方法的。
编写如下测试代码即可查看 php://filter/read=string.strip_tags/resource=php://input 的效果:

1
readfile('php://filter/read=string.strip_tags/resource=php://input');

image可见,<?php exit; ?>被去除了。这里有一个很有意思的地方,如果代码是

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

仅仅是加了一个echo,则会返回字符串加字符串长度image回到上面的题目,我们最终的目的是写入一个webshell,而写入的webshell也是php代码,如果使用strip_tags同样会被去除。万幸的是,php://filte允许使用多个过滤器,我们可以先将webshell用base64编码。在调用完成strip_tags后再进行base64-decode。“死亡exit”在第一步被去除,而webshell在第二步被还原。
最终的数据包如下:image然后可以看到,shell.php中的代码也写入成功了!太强了!image除此之外,我们还可以利用rot13编码独立完成任务。原理和上面类似,核心是将“死亡exit”去除。<?php exit; ?>在经过rot13编码后会变成<?cuc rkvg; ?>,在PHP不开启short_open_tag时,php不认识这个字符串,当然也就不会执行了:image当然,这个方法的条件就是不开启短标签。

Reference

http://rk700.github.io/2015/03/25/php-protocol-filter/
https://blog.csdn.net/ni9htmar3/article/details/69812306
https://www.leavesongs.com/PENETRATION/php-filter-magic.html?page=1#reply-list