前两天参加了浙江省省赛,从预赛到决赛总共考到了两题反序列化,赛后复现学到了一些新知识,这里做下记录
反序列化的基础知识,之前已经在博客做过总结
常用魔法函数
| 常用魔法函数 | 定义 | 
|---|---|
| __construct() | 在创建对象时候初始化对象,一般用于对变量赋初值。创建一个新的类时,自动调用该方法 | 
| __destruct() | 和构造函数相反,当对象所在函数调用完毕后执行.即当一个类被销毁时自动调用该方法 | 
| __toString() | 当对象被当做一个字符串使用时调用。 | 
| __sleep() | 当调用 serialize()函数时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()。这就允许对象在被序列化之前做任何清除操作 | 
| __wakeup() | 反序列化恢复对象之前调用该方法.当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数 | 
| __invoke() | 把一个实例对象当作函数使用时自动调用 | 
| __call() | 当调用对象中不存在的方法会自动调用该方法。 | 
| __get() | 在调用私有属性的时候会自动执行 | 
| __isset() | 在不可访问的属性上调用isset()或empty()触发 | 
| __unset() | 在不可访问的属性上使用unset()时触发 | 
第一道反序列化
搭了环境,感兴趣的师傅可以复现下,复现地址: http://www.npfs06.top:32789/
| 1 | 
 | 
通过代码审计,选择A1类作为pop链的嵌入点
 
A8类作为pop链最终要调用的地方,A8类这里有一个
echo new $this->tmp1($this->tmp2);
存在可控类,想到可以利用php原生类进行路径和文件的读取
 
需要考虑的就是如何从A1类成功调用A8类,方法有两中,如下
pop1
| 1 | $a = new A1(); | 
首先是A1类
 
__wakeup:反序列化恢复对象之前调用该方法.当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数
我们令$this -> tmp1 = new A3(), 这样在反序列化过程中触发__wakeup方法,从而调用了A3类的hacking方法,我们看到A3类
 
令A3-> tmp2 = new A5() ,通过调用hacking方法,会跳转到A5的get_flag()方法,我们看到A5类
 
__call: 当调用对象中不存在的方法会自动调用该方法。
因为A5类中不存在get_flag()方法,因此会触发__call()方法,我们令$this->tmp1 = new A7()
那么接下去的$f()就是将实例对象A7作为函数调用,
我们看到类A7
 
__invoke() :把一个实例对象当作函数使用时自动调用
因为在A5中A7被当作函数调用,因此会触发类A7的__invoke方法,我们令$this->tmp2= new A6() 
来到类A6()
 
__toString: 当对象被当做一个字符串使用时调用
A7的echo "114514".$this->tmp2.$this->tmp1;将A6当作字符串使用了,因此会调用A6的__toString方法,我们令$this->tmp1 = new A8()
 
成功调用了A8的hack4fun方法
序列化传参之后成功到达最后一步
| 1 | ?DASCTF=O:2:"A1":2:{s:4:"tmp1";O:2:"A3":2:{s:4:"tmp1";N;s:4:"tmp2";O:2:"A5":2:{s:4:"tmp1";O:2:"A7":2:{s:4:"tmp1";s:12:"Hello World!";s:4:"tmp2";O:2:"A6":2:{s:4:"tmp1";O:2:"A8":2:{s:4:"tmp1";N;s:4:"tmp2";N;}s:4:"tmp2";N;}}s:4:"tmp2";N;}}s:4:"tmp2";N;} | 
 
pop2
| 1 | $a = new A1(); | 
| 1 | ?DASCTF=O:2:"A1":2:{s:4:"tmp1";O:2:"A3":2:{s:4:"tmp1";N;s:4:"tmp2";O:2:"A4":2:{s:4:"tmp1";O:2:"A6":2:{s:4:"tmp1";O:2:"A8":2:{s:4:"tmp1";N;s:4:"tmp2";N;}s:4:"tmp2";N;}s:4:"tmp2";N;}}s:4:"tmp2";N;} | 
 
文件读取
接下去就是原生类的利用了
 
我们需要通过传参DAS和CTF,调用原生类
可以进行文件操作的内置类:
| 类 | 描述 | 
|---|---|
| DirectoryIterator | 遍历目录 | 
| FilesystemIterator | 遍历目录 | 
| GlobIterator | 遍历目录,但是不同的点在于它可以通配例如/var/html/www/flag* | 
| SplFileObject | 读取文件,按行读取(默认只读第一行),多行读取需要遍历 | 
| finfo/finfo_open() | 需要两个参数 PHP扩展类 | 
首先通过内置类FilesystemIterator找到flag文件名
 
接下去就是要读取这个flag文件了,但是SplFileObject类只能读取第一行,通过测试发现文件中flag不在第一行,无法成功读取,比赛的时候就卡在这里了
新知识:SplFileObject类搭配伪协议可以实现多行文件内容读取
最终payload:
 
base64解密下就可以得到flag
 
第二道反序列化
复现地址 :http://www.npfs06.top:32791/
| 1 | 
 | 
这一题的链子比前一题简单,不过解题方法有两种
方法一
| 1 | Test::getFlag()<- Fun:__call() <- A:__get() <- B:__destruct() | 
入口为B类
 
最终调用点为Test类的getFlag方法
 
链子如下
 
| 1 | $a = new B(); | 
方法二
| 1 | Fun:__call() <- A:__get() <- B:__destruct() | 
入口还是为B类,不过最终调用点为Fun类的__call方法
我们令B类的B -> a=new A(),B -> p="cat /f*",
 
这样echo $this->a->$p;就会调用A类,
 
A类的return $this->a->$p();,我们将$this -> a = new Fun(),这里的$p就是我们在B类定义的p值,$p后面加上括号,会被识别为方法
 
__call: 当调用对象中不存在的方法会自动调用该方法。
因为Fun类中不存在$p方法,从而会调用Fun类中的__call方法
__call方法中有call_user_func,并且参数可控,我们可以直接构造system('cat /f*'),
 
这里的$f变量不存在,并不影响system的执行
将Fun类的$this->func = system   从而直接实现命令执行
 
| 1 | $a = new B(); | 
因为存在私有类
 
我们需要进行urlencode编码
PHP 序列化的时候 private和 protected 变量会引入不可见字符
%00,%00类名%00属性名为private,%00*%00属性名为protected,注意这两个 %00就是 ascii 码为0 的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得清楚
方法一的最终payload为:
 
注意下,图中框着的地方要修改为大于2的数值
| 1 | O%3A1%3A%22B%22%3A3%3A%7Bs%3A1%3A%22p%22%3Bs%3A3%3A%22111%22%3Bs%3A1%3A%22a%22%3BO%3A1%3A%22A%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A3%3A%22Fun%22%3A1%3A%7Bs%3A9%3A%22%00Fun%00func%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A4%3A%22Test%22%3Bi%3A1%3Bs%3A7%3A%22getFlag%22%3B%7D%7D%7D%7D | 
方法二的最终payload为:
| 1 | O%3A1%3A%22B%22%3A3%3A%7Bs%3A1%3A%22p%22%3Bs%3A7%3A%22cat+%2Ff%2A%22%3Bs%3A1%3A%22a%22%3BO%3A1%3A%22A%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A3%3A%22Fun%22%3A1%3A%7Bs%3A9%3A%22%00Fun%00func%22%3Bs%3A6%3A%22system%22%3B%7D%7D%7D | 
也是一样,将元素个数2修改为大于2的值
 
关于上面为什么要修改元素个数的解释
最开始是以为修改元素是为了绕过fun类的 __weakup方法
 
但是最后发现,不论是否修改元素个数,最终还是会输出Don't serialize me
整不明白了,可能是php版本问题,等官方wp出来再补充