前两天参加了浙江省省赛,从预赛到决赛总共考到了两题反序列化,赛后复现学到了一些新知识,这里做下记录
反序列化的基础知识,之前已经在博客做过总结
常用魔法函数
常用魔法函数 | 定义 |
---|---|
__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出来再补充