0 前言
花了一个晚上把prize1给复现了一下,期间遇到了一些坑点,当然也学到了很多新姿势(题的质量真是高),这篇将整个过程记录下来
本篇参考了atao
xenny
xilzy
师傅的wp,将其中的内容做了整理,希望可以把题目对应的知识点梳理清楚
1 题目
源码:
1 | <META http-equiv="Content-Type" content="text/html; charset=utf-8" /> |
1.1 代码审计
首先是一个getflag
类,内容就是输出$FLAG
,触发条件为__destruct
;第二个类是A
,作用有两个,一个是写文件,一个是读文件,写入数据和读取对象都是POST[0]
然后就是对GET[0]
的关键字判断,通过后反序列化GET[0]
1.2 思路
这里因为关键字对flag
有过滤,所以无法直接触发getflag
类;转眼去看A
类,既然有任意内容写入
+任意文件读取
+类
,优先考虑phar
,phar反序列化的基础利用请读者自行先去了解,这里不做介绍。
那我们的操作就是先利用A类的写文件功能写入一个phar文件,其中phar文件的metadata部分设置为getflag类,这样phar://读取之后,其中的metadata部分的数据就被反序列化,getflag就生成了,再最后程序结束触发__destruct获取flag
1.3 问题
思路是正确的,但是还需要处理细节。
①__destruct的触发
源码在最后给了一个throw Error
,这个的作用是什么呢,我们可以在本地试试


可以看到这个throw Error
使得我们的类没有被销毁(为什么没有被销毁放在下面讲),所以如何触发__destruct方法成为了一个问题
②关键字过滤的绕过
在写入文件时,我们需要带入的getflag
类中还是包含了flag
这个关键字,还是会被拦住。所以我们还需要想办法绕过这个关键字,同时还得保证phar://读取时仍可以反序列化其中的数据(这才是关键
2 问题解决&知识点梳理
2.1强制GC触发__destruct
在PHP中,正常触发析构函数(__destruct)有三种方法:
①程序正常结束
②主动调用unset($aa)
③将原先指向类的变量取消对类的引用,即$aa = 其他值;
前两种很好理解,我们来讲讲第三种
PHP中的垃圾回收Garbage collection
机制,利用引用计数和回收周期自动管理内存对象。当一个对象没有被引用时,PHP就会将其视为“垃圾”,这个”垃圾“会被回收,回收过程中就会触发析构函数
1 | class bilala{ |
所以在这道题中,我们可以利用取消原本对getflag
的引用,从而触发他的析构函数。
2.1.1 思路
操作如下,在phar的metadata中写入的内容为a:2:{i:0;O:7:"getflag":0:{}i:0;N;}
这样的话,当phar://反序列化其中的数据时(反序列化时是按顺序执行的),先反出a[0]的数据,也就是a[0]=getflag类,再接着反序列化时,又将a[0]设为了NULL,那就和上述所说的一致了,getflag类被取消了引用,所以会触发他的析构函数,从而获得flag
2.1.2 修改phar文件
但新的问题又随之产生了,我们在phar中无法生成上述的字符串内容,我们只能生成a:2:{i:0;O:7:"getflag":0:{}i:1;N;}
,(这里先贴上生成phar文件的代码
1 |
|
(不知道为什么无法生成a:2:{i:0;O:7:"getflag":0:{}i:0;N;}
的可以将代码中的1=>null
改成0=>null
试试)
(无法生成的话记得修改php.ini
中的phar的readonly
为off
并去掉这行前边的分号,具体操作百度)
生成后查看phar
(我这里为了方便查看metadata数据所以选择用记事本打开,但这其实为我埋下了坑)
在复现时,我以为直接将那个1改为0,再重新计算签名就好了,然而实际上这行不通,在记事本中修改的话,实际上被修改的不止你改了的数字,还有后面的签名以及签名计算方法等等,所以千万别在记事本里改!!!(坑死我了)
正确方法是在010editor中修改,将对应的位由1改成0,然后保存
2.1.3 phar签名
phar文件是修改成功了,但这个时候这个phar是处于损坏状态的,因为我们修改了前面的数据导致后面的签名对不上。这个时候,我们还需要手动计算出这个新phar文件的签名,查看PHP手册找到phar的签名格式
我们刚刚的phar的签名标志位为0x0002,为SHA1
签名,所以我们要计算的是出的字节是[-28:-8]
,用脚本计算我们新的phar文件的签名,并重新写入文件(也可以导出为新文件)
1 | from hashlib import sha1 |
至此,第一个问题算是解决了(这个文件留着有用)
2.2 PHP异常
PHP中的错误级别:
致命错误 E_ERROR, 语法错误 E_PARSE, 警告错误 E_WARNING, 通知错误 E_NOTICE
其中前两种会导致程序异常退出(中止),所以程序本该释放内存等这些操作也就无法完成了,也就无法触发析构函数
而后两种只是抛出异常,但仍会继续执行程序
2.3 数组绕过preg_match
这个不用多说了,就是在题中POST[0]
传入数组即可绕过关键字检测,就可以直接写入phar文件的内容了,无需对phar文件做额外处理,然后直接获取flag
2.4 phar://支持的后缀
除了.phar
可以用phar://读取,gzip
bzip2
tar
zip
这四个后缀同样也支持phar://读取
具体可参考guoke师傅的文章
所以在此题中,可以对phar.phar
文件做以上这些处理,使其成为乱码,从而绕过关键字的检测。
①gzip
直接在Linux中gzip压缩一下,然后通过POST[0]上传这个文件,再读取flag
②tar
在guoke师傅的文章中有PHP对tar文件的phar://的底层处理源码分析,其中有涉及到.phar/.metadata
这个文件,这里只讲怎么操作
先新建一个.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
可以看到)
3 题解
此处再梳理一下思路,我们先传入构造的phar文件内容,但是在传入时我们需要先绕过preg_match的检测(数组绕过或者tar,gzip),传入后我们再利用phar://tmp/a.txt
读取文件。读取时,会反序列化其中的metadata数据(我们构造的数据),在反序列化a:2:{i:0;O:7:"getflag":0:{}i:0;N;}
时,又会因为类被取消引用从而触发GC,从而触发getflag类的析构函数,从而获取flag
3.1 ①数组绕过
首先要完成2.1中的操作,获得一个正确签名的且有我们想要的metadata数据的phar文件。
然后在POST[0]时传入数组即可,具体看脚本
1 | import requests |
3.2 ②gzip绕过
同样的也先完成2.1中的操作获得phar文件,再将此phar文件如2.4中的①一样操作,获得phar.phar.gz
文件,再利用3.1中的脚本,将文件名phar.phar
改成phar.phar.gz
即可,当然也可以直接用脚本进行gzip压缩并完成后续一系列操作
1 | import requests |
3.3 ③tar绕过
(无需先完成2.1中获得新phar文件的操作了)直接如2.4②中那般操作,获得tartest.tar.gz
文件,然后利用3.1中的脚本,改对应文件名即可
4 疑惑解答
这里就列出一些大家可能存在疑问的地方,希望可以帮助到大家(还有疑问的可以在评论里留言)
Q:为什么getflag类的析构不会触发,而A类的析构可以触发
A:反序列化A类时用的是unserialize($_GET[0])
语句,用完就被丢了,触发GC(前边提到了),进而触发析构
Q:为什么phar://tmp/a.txt
会触发反序列化,不是说只能那五个后缀才可以吗
A:一个文件是什么类型不取决于文件名中的后缀,而是由他的文件格式决定的(放010editor下可以看到)
5 吐槽
当时用的xenny
师傅的脚本,就是writeup里admin的题解,结果他的脚本中有小瑕疵,然后我一直跑不出来,后来用的xilzy
师傅的脚本才出来,然后我就仔细比对两个之间的差别,找了好久才发现xenny的脚本中少了个1
如果师傅们用的这个脚本可以留意一下(后来和群主反馈过了
555,在这里浪费了好长时间