【CTF竞赛】[NISACTF 2022]popchains—PHP反序列化POP链
[NISACTF 2022]popchains—PHP反序列化POP链
一、看题
1 | Happy New Year~ MAKE A WISH |
是一道非常经典的PHP反序列化的POP链调用的题,非常有参考学习价值
二、横扫pop链盲区
1、反序列化中常见起点函数
- __wakeup 一定会调用:在反序列化之前被调用
- __destruct 一定会调用:当一个对象销毁时被调用。及反序列化之后的对象执行的内容走完之后,无路可走,他就会走
- __toString 当一个对象反序列化后被当做字符串调用
2、反序列化中常见的中间跳板
- __toString 当一个对象被当做字符串使用
- __get 读取不可访问或不存在属性时被调用
- __set 当给不可访问或不存在属性赋值时被调用
- __isset 对不可访问或不存在的属性调用isset()或empty()时被调用
- __invoke:当脚本尝试将对象调用为函数时触发。形如 $this->$func();
3、反序列化中常见的终点函数
- __call 调用不可访问或不存在的方法时被调用
- call_user_func 一般php代码执行都会选择这里
- call_user_func_array 一般php代码执行都会选择这里
三、解题
1、锁定入口类
先看非class 类的部分
1 | if(isset($_GET['wish'])){ |
就是获取一个get 参数,之后对其值进行反序列化操作,反序列化一个实例对象出来。既然知道对值进行了反序列化,那么这个wish
的值一定是某个对象序列化后的值。通读下文知道,这里有三个class 类,也就是说,答案就是一下三个其中一个class 对象的序列化值。
那具体是谁?我们知道,有三个入口函数,分别是__wakeup
、__destruct
和__toString
函数。我们逐步分析
__wakeup
函数:在反序列化的对象被激活之前,会走__wakeup
函数,在三个class 类中,只有Road_is_Long
类有这个函数,也就是说Road_is_Long
类是可以作为入口类- __destruct 函数:三个class 类中没有这个函数,所以也就不能用这个来评估入口类
- __toString 函数:这个函数在被作为入口评估函数之前,需要在非class 类的部分中,反序列化后被当做字符串调用才能作为入口评估函数。比如:
echo @unserialize($\_GET['wish']);
,所以在本题中也不能被作为入口评估函数
这样,我们锁定了入口的class 类 Road_is_Long
,那么,我们需要写代码了
新建一个php文件,把这个入口的class类Road_is_Long
复制过去,并删除他所有的魔术方法。再对这个类做实例化和序列化
1 | <?php |
OK,好极了,那么这里剩下了两个变量,这两个变量的值是多少需要往下走。
2、__wakeup 函数
之后就该读__wakeup入口函数的内容了
1 | public function __wakeup(){ |
这里提到了 page 变量,对这个变量进行正则匹配,过滤了很多的协议。如果匹配成功,则更改这个变量的值为index.php
。
- 思路一:大家可能会尝试绕过这个正则匹配,继续往后走。但问题是不管有没有绕过这个函数,当这个函数走完之后走哪个函数呢?已经到了无路可走的时候了,而这个class 中既无
__destruct
函数,也无其他的终点函数,代码直接走到头了。 - 思路二:page 变量在进行正则匹配的时候,是会被当作字符串去看待。有没有感觉眼熟,没错,如果此时page变量的值正好是某个对象的话,则会触发这个对象的
__toString
函数。而其本身Road_is_Long
是已经被反序列化出来的对象,且具备__toString
函数
则page 变量的内容确定,代码如下:
1 | <?php |
这样子,将会走到__toString
函数里面去,就剩string变量的内容了
3、__toString 函数
之后读__toString 函数的内容
1 | public function __toString(){ |
非常简单,只有一行代码,但是基础薄弱的伙伴看到会感觉懵了很,看不懂。其实这个很简单,我们拆开看
- 第一部分:
$this->string
,这里其实说的就是这个对象的$string
变量,也就是我们剩下的第二个变量。 - 第二部分:
string->page
,这里是个迷惑行为,这个string
指我们的变量,第二个page
变量指的是其他。它并不是我们的page变量,而是随意的一个内容均可,如string->abc
,对这个题是没有任何影响。
1、我们知道一个实例化的对象,才可以用->,如我们写的:
1
2 $ril = new Road_is_Long();
$ril->page = $ril;中$ril是对象,才可以用->来调用page变量
2、同样的$this->string->page
意味$this->string是一个对象,看整体
$this->string ->page
对象 的 page变量
所以,第二个变量string也是一个对象。那具体是谁的对象?
- 思路一:那就看谁的class类里面有page变量咯。但是只有
Road_is_Long
对象才有page变量啊,啊?这怎么回事儿,感觉脑袋有点懵,怎么又调用回来了? - 思路二:已经走到了反序列化中常见的中间跳板函数。__get 当读取不可访问或不存在属性时被调用。如果这个string 的值是
Make_a_Change
类的实例对象,而这个类中没有page 变量,但是有get函数
啊。这样一来,就走到这个类里面的get 函数
里面了
则string 的内容确定,如下:
1 | <?php |
那么这个时候又多出来了一个$effort
变量,需要确定这个变量的值是多少
4、__get 函数
那继续看__get
函数
1 | public function __get($key){ |
正好,这里提到了effort 变量。
- $function = $this->effort; 将effort变量的值给$function 变量,这没啥,可关键在第二行
- return $function(); 这什么鬼,一个变量后面加个(),会不会懵。但我们在往上看中间跳板的那些函数中,有无与这个相关。__invoke:当脚本尝试将对象调用为函数时触发。欸,如果此时effort 变量的值是一个对象的话,$function() 不就相当于把这个对象当成函数来调用了吗。而在
Try_Work_Hard
类中正好有这个invoke
函数
此时,effort变量的值确定:
1 | <?php |
注意:我们拿过来的类,需要把里面所有的方法都去掉,只留下变量即可。
这下又多出来一个未知的变量:Try_Work_Hard
类里面的 $var
变量。继续往后看
5、__invoke 函数
1 | public function __invoke(){ |
也是只有一行代码,把本对象的var 变量的值做为参数传入本对象的append 方法中,噢,看来要迎来结局了
看本对象的append 方法
1 | public function append($value){ |
进行了文件包含,看来整个代码的利用核心就在这里,那这个$var
的值,就是我们最终的payload了。
欸,但是又出现了一个点,如下
1
2
3 class Try_Work_Hard{
protected $var;
}$var变量是由protected 进行修饰,我们实例化的对象无法进行调用
也就是说,我们无法写$twh->var;这样的代码
所以只能在class类中写入,如下
1 | <?php |
OK,整个代码分析结束
四、提交答案
看下整个代码的输出结果
还挺长的,把双引号中的拿过去
噢, 啥也没有输出。
这个时候需要把我们的内容做一个url编码,当然也可以直接在代码里面做,在输出的时候添加一个urlencode即可,如下
1 | <?php |
这个时候,把我们的这一坨给他
OK,非常的完美
针对这道题而言,只需要把/etc/passwd
换成/flag
即可,甚至可使用php伪协议