# PHP 序列化问题 -> 主要是找链 分析代码
# 反序列化魔术方法
1 2 3 4 5 6 7 8 9
| __construct() __destruct() __toString() __sleep() __wakeup() __get() __set() __invoke() __call()
|
1 2 3 4 5 6 7 8 9 10 11
| __wakeup() __sleep() __destruct() __call() __callStatic() __get() __set() __isset() __unset() __toString() __invoke()
|
# 简介
序列化其实就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化。
在网上找到一个比较形象的例子
比如:现在我们都会在淘宝上买桌子,桌子这种很不规则的东西,该怎么从一个城市运输到另一个城市,这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)。
php 将数据序列化和反序列化会用到两个函数
serialize 将对象格式化成有序的字符串
unserialize 将字符串还原成原来的对象
序列化的目的是方便数据的传输和存储,在 PHP 中,序列化和反序列化一般用做缓存,比如 session 缓存,cookie 等。
# 常见的序列化格式
了解即可
- 二进制格式
- 字节数组
- json 字符串
- xml 字符串
# 案例引入
简单的例子 (以数组为例子)
1 2 3 4 5
| <?php $user=array('xiao','shi','zi'); $user=serialize($user); echo($user.PHP_EOL); print_r(unserialize($user));
|
输出:
1 2 3 4 5 6 7
| a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";} Array ( [0] => xiao [1] => shi [2] => zi )
|
我们对上面这个例子做个简单讲解,方便大家入门
1 2 3 4 5 6
| a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";} a:array代表是数组,后面的3说明有三个属性 i:代表是整型数据int,后面的0是数组下标 s:代表是字符串,后面的4是因为xiao长度为4 依次类推
|
序列化后的内容只有成员变量,没有成员函数,比如下面的例子
1 2 3 4 5 6 7 8 9 10
| <?php class test{ public $a; public $b; function __construct(){$this->a = "xiaoshizi";$this->b="laoshizi";} function happy(){return $this->a;} } $a = new test(); echo serialize($a); ?>
|
输出 (O 代表 Object 是对象的意思,也是类)
1
| O:4:"test":2:{s:1:"a";s:9:"xiaoshizi";s:1:"b";s:8:"laoshizi";}
|
而如果变量前是 protected,则会在变量名前加上 \x00*\x00
,private 则会在变量名前加上 \x00类名\x00
, 输出时一般需要 url 编码,若在本地存储更推荐采用 base64 编码的形式,如下:
1 2 3 4 5 6 7 8 9 10 11
| <?php class test{ protected $a; private $b; function __construct(){$this->a = "xiaoshizi";$this->b="laoshizi";} function happy(){return $this->a;} } $a = new test(); echo serialize($a); echo urlencode(serialize($a)); ?>
|
这时候输出则会导致不可见字符 \x00
的丢失 (故要 urlencode)
1
| O:4:"test":2:{s:4:" * a";s:9:"xiaoshizi";s:7:" test b";s:8:"laoshizi";}
|
# 反序列化绕过小 Trick
# php7.1 + 反序列化对类属性不敏感
我们前面说了如果变量前是 protected,序列化结果会在变量名前加上 \x00*\x00
但在特定版本 7.1 以上则对于类属性不敏感,比如下面的例子即使没有 \x00*\x00
也依然会输出 abc
1 2 3 4 5 6 7 8 9 10 11
| <?php class test{ protected $a; public function __construct(){ $this->a = 'abc'; } public function __destruct(){ echo $this->a; } } unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
|
# 绕过__wakeup (CVE-2016-7124)
版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
利用方式: 序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
对于下面这样一个自定义类
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class test{ public $a; public function __construct(){ $this->a = 'abc'; } public function __wakeup(){ $this->a='666'; } public function __destruct(){ echo $this->a; } }
|
如果执行 unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
输出结果为 666
而把对象属性个数的值增大执行 unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}');
输出结果为 abc
# 绕过部分正则
preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头,这在曾经的 CTF 中也出过类似的考点
- 利用加号绕过(注意在 url 里传参时 + 要编码为 %2B)
- serialize (array (a));//a 为要反序列化的对象 (序列化结果开头是 a,不影响作为数组元素的 $a 的析构)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <?php class test{ public $a; public function __construct(){ $this->a = 'abc'; } public function __destruct(){ echo $this->a.PHP_EOL; } }
function match($data){ if (preg_match('/^O:\d+/',$data)){ die('you lose!'); }else{ return $data; } } $a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
$b = str_replace('O:4','O:+4', $a); unserialize(match($b));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
|
# 利用引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php class test{ public $a; public $b; public function __construct(){ $this->a = 'abc'; $this->b= &$this->a; } public function __destruct(){ if($this->a===$this->b){ echo 666; } } } $a = serialize(new test());
|
上面这个例子将 $b
设置为 $a
的引用,可以使 $a
永远与 $b
相等
# 16 进制绕过字符的过滤
1 2 3 4
| O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";} 可以写成 O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";} 表示字符类型的s大写时,会被当成16进制解析。
|
这里给出一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php class test{ public $username; public function __construct(){ $this->username = 'admin'; } public function __destruct(){ echo 666; } } function check($data){ if(stristr($data, 'username')!==False){ echo("你绕不过!!".PHP_EOL); } else{ return $data; } }
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}'; $a = check($a); unserialize($a);
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}'; $a = check($a); unserialize($a);
|
# PHP 反序列化字符逃逸
# 情况 1:过滤后字符变多
首先给出本地的 php 代码,很简单不做过多的解释,就是把反序列化后的一个 x 替换成为两个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php function change($str){ return str_replace("x","xx",$str); } $name = $_GET['name']; $age = "I am 11"; $arr = array($name,$age); echo "反序列化字符串:"; var_dump(serialize($arr)); echo "<br/>"; echo "过滤后:"; $old = change(serialize($arr)); $new = unserialize($old); var_dump($new); echo "<br/>此时,age=$new[1]";
|