0x00 前言
新生赛的一道题,在反序列化里算是比较简单的,有必要专门拿来写博客吗?
有。其一,这道题能锻炼我的逻辑,写下来更有利于我的思考和逻辑的培养;其二,反思不看重题的难度,更看重自己学到什么。
本篇文章我会说得比较详细,请读者细细品味,多加思考。
0x01 题目
题目给了四个类:
class000
class001
class002
class003
然后是主程序:
写前须知:
- 了解:
__invoke()
、__set()
等魔术方法 - 了解
PHP
反序列化
0x02 分析
我们的目的是什么?我们要想办法得到flag
。因为本题没有直接提示flag
在哪,所以我们先尝试getshell
。我们要怎么实现getshell
?getshell
的方式有很多,在本题我们看到了一个危险函数eval()
和似乎可控的变量,我们需要利用这段程序实现getshell
。那怎么利用eval()
和似乎可控的变量实现getshell
呢,这需要下文的反序列化构造POP
链来实现。
概念和宏析
先解释一些概念(我自己编的概念,方便逻辑推理):
起点&终点
- 起点:反序列化实例化对象后自动触发的点为起点,会引发程序按照特意的目的进行,起点一般是某些魔术方法。
- 终点:可以直接实现
getshell
的点。终点一般是危险函数。
最里层&最外层
对象嵌套:举例说明:A类实例的一个属性是B类实例,B类实例的一个属性是C类实例······,以此类推。
最里层:顾名思义,就是对象嵌套里的最里层的那个对象。
最外层:对象嵌套里的最外层的那个对象
宏观分析反序列化:
当我们把序列化的数据交给相关程序时,反序列化函数会生成一个所属类的实例。如果只是一个简单的实例,那么程序就像死水一样毫无动静,但是如果这个实例有魔术方法,当类的实例满足特定的条件时程序就会自动调用这些方法,那么整个程序就动起来了。只有程序动起来才能实现我们的目的。
我们需要一个起始魔术方法也就是上文所说的起点,这个魔术方法所需的条件是必定会满足的,这样程序就可以在反序列化生成实例后自动运行。你能在本题找到符合的魔术方法吗?
没错,本题的起点就是class000
的析构函数__destruct()
。起点不一定在最外层对象,但是起着运行特定程序的作用,所以写反序列化,我们必须要找到这样一个起点。
我们同样需要一个终点,在终点我们可以直接实现getshell
。这样的终点往往是具有危险函数的点,比如本题的:
现在我们知道了触发程序的起点(不一定在最外层的对象),和可以getshell
的终点(一般在最里层的对象)。
方法和原则
反序列化的一般方法即是从最里层推到最外层。何为最里层,就是直接拥有危险函数的类的实例。对于本题而言,就是class003
。
class003
的eval()
是危险函数,我们希望$str
是可控变量。在反序列化里,所有属性都是可控变量。
要使用eval()
需要先使用evvval()
,这同样是反序列化的一个重要特征:所有方法的触发都是有条件的。
现在我们理清楚了反序列化程序的两个基本元素:
- 所有属性都是可控变量
- 所有方法的触发都是有条件的
那么我们就从最里层class003
,依照上述原则一层一层往外推。最后我再补充一点,对于各个方法的参数我们通常不主动关注,随着方法的回推参数自然会一层层回推。
不断的回推
我们要使用class003
的eval()
,那么首先要调用class003
的evvval()
。哪里可以调用class003
的evvval()
呢?
把目光转向class002
:
class002
的dangerous()
方法可以调用evvval()
。因为evvval()
是class003
的方法,所以$shaattt
必须是class003
的实例。那么现在的问题是:我们该怎么调用class002
的dangerous()
呢?
再细看一下dangerous()
,这么一个方法是有参数的,通观所有类,只有class002
自己的__set()
魔术方法可以满足。看看__set()
魔术方法的触发条件:
在给不可访问的(
protected
或者private
)或者不存在的属性赋值的时候,会被调用__set()
魔术方法
上述条件是很广泛的,可以是存在的属性也可以是不存在的属性。我们不妨都找找(寻找的线索就是赋值操作):
第一处:
看起来很怪,$a更像是一个实例或者方法而不是属性。
第二处:
payload
属性是所有类都没有的,符合上面的条件。那么class001
的$a
应该是class002
,class001
的$payl0ad
应该是字符串dangerous
(为什么是字符串,这属于PHP
基本功)。要满足上述的赋值操作,需要满足__invoke()
魔术方法,看看这个魔术方法的使用条件:
__invoke() 当脚本尝试将对象调用为函数时触发,该对象必须是直接拥有
__invoke()
魔术方法的对象
这里有个细节,上述的对象必须是直接拥有__invoke()
魔术方法的对象,不能是嵌套的外层的对象。
现在想想该怎么触发__invoke()
魔术方法呢?我们刚才找到了这一处:
如果class000
的$what
是class001
,那么class000
的$a
就是class001
?等等,class000
好像没有$a
这个属性吧?完蛋了吗,好像推不出来了!你会怎么想呢,不妨先不往下看,自己想几分钟。
没错,给class000
实例外面再套一层class001
实例,那么就相当于class000
实例有$a
属性了。注意!这里的class001
实例和上面提到的需要触发__invoke()
魔术方法的class001
实例是不同的实例。
最后再给class000
的$payl0ad
属性赋个值,别触发die()
即可。
流程和POP
大概的流程,也就是嵌套关系,可以粗糙表示为:
class003 < class002 < class001 < class000 <class0001
附上POP
链:
1 |
|
0x03 小结
回顾此题时,好像没有那么难。当时因为__invoke()
的直系问题使我的思路卡了好久,所幸又认真看了相关的概念解释才得以解决该题目。所以基础非常重要,基础是推理的根据,就像数学的公理和推论。