序列化与反序列化
为了有效地存储或传递数据,同时不丢失其类型和结构,经常需要利用序列化和反序列化函数对数据进行处理
序列化函数返回字符串,此字符串包含了表示值的字节流,可以存储于任何地方
反序列化函数对单一的已序列化的变量进行操作,将其转换回原来的值
序列化
PHP中的序列化函数是serialize()
NULL和标量类型数据的序列化
- NULL
在PHP中,NULL被序列化为N

- bool型
bool型数据被序列化为b:<digit>
。其中,
当bool型数据为false时,

- int型
int型数据被序列化为i:<number>
。其中,
如果被序列化的数字溢出则被序列化为浮点型
如果序列化后的数字超过这个范围,则反序列化时不会返回期望的数值

- double型
double型数据被序列化为d:<number>
,其中,
如果序列化(负)无穷大,则
如果序列化后的数字小于PHP所能表示的最小精度,则反序列化时返回0
如果被序列化的数据非数字,则被序列化为NAN,NAN反序列化时返回0

- string型
string型数据被序列化为s:<length>:"<value>"

简单复合型数据的序列化
- 数组
数组通常被序列化为a:<n>:{<key 1>;<value 1>;<key 2>;<value 2>;···<key n>;<value n>}
其中n
表示数组元素的个数,<key>
代表元素下标,<value>
代表相应的值
数组下标的类型只能为整型和字符串型,数组序列化后的格式与整型和字符串型数据序列化后的格式相同
数组元素值可以是任意类型,其序列化后的格式与其所对应的类型序列化后的格式相同

- 对象
对象通常被序列化为O:<length>:"<class name>":<n>:{<field name 1>;<field value 1>;<field name 2>;···<field name n><field value n>}
其中,<length>
代表对象的类名的字符串长度;<class name>
代表对象的类名;<n>
表示对象中的字段个数
这些字段包括在对象所在类及其祖先类中用var
、public
、protected
、private
声明的字段,但是不包括用static
和const
声明的静态字段,也就是说只有实例字段
<field name>
表示字段名,<field value>
表示字段值
字段名是字符串型,序列化后的格式与字符串型数据序列化后的格式相同
字段值可以是任意类型,其序列化后的格式与其所对应的类型序列化后的格式相同

其中,O表示object,3表示对象的类名的字符串长度为3,Foo表示对象的类名是Foo,2表示有两个数据字段
第一个字段是s:10:"aMamberVar";s:26:"aMamberVar Member Variable";
s:10:"aMamberVar";
表示字段的类型是string,字段长度是10,字段名称是aMamberVar
s:26:"aMamberVar Member Variable";
表示字段的类型是string,字段长度是26,字段值是aMamberVar Member Variable
第二个字段是s:9:"aFuncName";s:11:"aMemberFunc";
s:9:"aFuncName";
表示字段的类型是string,字段长度是9,字段名称是aFuncName
s:11:"aMemberFunc";
表示字段的类型是string,字段长度是11,字段值是aMemberFunc
反序列化
PHP中的反序列化函数是unserialize()
若被序列化的变量是一个对象,在重新构造对象后,会自动调用__wakeup成员函数(如果存在)

反序列化漏洞
反序列化漏洞产生的主要原因是;
- unserialize函数的参数可控
- 存在魔法函数
魔法函数
__construct
、__destruct
、__call
、__callStatic
、__get
、__set
、__isset
、__unset
、__sleep
、__wakeup
、__toString
、__invoke
、__set_state
、__debugInfo
等成员函数在PHP中被称为魔法函数
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
在命名自己的类方法时不能使用这些名称,除非是想使用其魔法函数功能
__construct和__destruct
- __construct
用法:
void __construct([mixed $args[, $...]])
PHP5允许开发者在一个类中定义一个方法作为构造函数
具有构造函数的类会在每次创建新对象时先调用此方法,所以__construct函数非常适合在使用对象之前做一些初始化工作
- __destruct
用法:
void __destruct(void)
PHP5引入了析构函数的概念,这类似于其他面对对象的语言,如C++
__destruct函数是对象被销毁的时候进行调用,通常PHP在程序块执行结束时进行垃圾回收,这将进行对象销毁,然后自动触发__destruct魔术方法
- 代码示例
<?php
highlight_file(__FILE__);
class MyTestClass {
function __construct() {
print "In constructor<br>";
$this->name="MyTestClass";
}
function __destruct() {
print "Destructing " .$this->name."\n";
}
}
$obj = new MyTestClass();
?>

创建MyTestClass类的新对象时,会调用__construct函数,输出In constructor
对象被销毁时,会调用__destruct()函数,输出Destructing MyTestClass
__sleep和__wakeup
- __sleep
serialize函数会检查类中是否存在__sleep函数,如果存在,该函数会先被调用,然后才执行序列化操作
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组
如果该函数未返回任何内容,则NULL被序列化,并产生一个E_NOTICE级别的错误
__sleep函数不能返回父类的私有成员的名字,这样做会产生一个E_NOTICE级别的错误,该函数可以用serializable接口来代替
__sleep函数常用于提交未提交的数据或进行类似的清理操作
- __wakeup
unserialize函数会检查是否存在__wakeup函数,如果存在,则会先调用__wakeup函数,预先准备对象需要的资源
__wakeup函数常用在反序列化操作中,例如,重新建立数据库连接或执行其他初始化操作
- 代码示例
<?php
highlight_file(__FILE__);
class Foo {
function aMemberFunc() {
$this->name="aFuncName";
print $this->name;
}
function __wakeup() {
echo "<br>wakeup<br>";
}
function __sleep() {
echo "<br>sleep<br>";
return array('name');
}
}
$tr = 'O:3:"Foo":1:{s:9:"aFuncName";s:11:"aMemberFunc";}';
$ttr = unserialize($tr);
var_dump($ttr);
$foo = new Foo;
$foo->name="abc";
$tr=serialize($foo);
print $tr;
?>

反序列化后,会自动调用__wakeup函数,输出wakeup
序列化对象后,会自动调用__sleep函数,输出sleep
字符逃逸
过滤后增加字符
<?php
function change($str){
return str_replace("x","xx",$str);
}
$a = $_GET['a'];
$b = 'flag';
$arr = array($a,$b);
$old = change(serialize($arr));
$new = unserialize($old);
echo "<br>字符串:";
echo var_dump($arr);
echo "<br>";
echo "<br>序列化字符串:";
var_dump(serialize($arr));
echo "<br>";
echo "<br>过滤后:";
var_dump($old);
echo "<br>";
echo "<br>反序列化字符串:";
var_dump($new);
echo "<br>";
echo "<br>逃逸前:";
echo '<br>$arr[1]='.$arr[1];
echo "<br>";
$arr = $new;
echo "<br>逃逸后:";
echo '<br>$arr[1]='.$arr[1];
echo "<br>";
?>
传入a=xxxxxxxxxxxxxxxxx";i:1;s:4:"flag";}

下面讲讲原理
在字符串反序列化时,unserialize函数是依据字符串长度来判断字符串范围的,例如:

此字符串反序列化时,会将双引号内的34个字符视为数组的第一个元素
如果双引号内字符不是对应的个数,那么反序列化就会返回false
传入的";i:1;s:3:"wtf";}
是17个字符,由于str_replace函数会将x
替换成xx
,17个x
会变成34个x
,多出来的17个x
造成了";i:1;s:3:"wtf";}
的溢出,而"
闭合了字符串,成功逃逸,可以被反序列化
最后的;}
闭合反序列化全过程,导致原来的";i:1;s:4:"flag";}"
被截断,但不影响反序列化过程
过滤后减少字符
<?php
function change($str){
return str_replace("xx","x",$str);
}
$a = $_GET['a'];
$s = 'wtf';
$b = $_GET['b'];
$arr = array($a,$s,$b);
$old = change(serialize($arr));
$new = unserialize($old);
echo "<br>字符串:";
echo var_dump($arr);
echo "<br>";
echo "<br>序列化字符串:";
var_dump(serialize($arr));
echo "<br>";
echo "<br>过滤后:";
var_dump($old);
echo "<br>";
echo "<br>反序列化字符串:";
var_dump($new);
echo "<br>";
echo "<br>逃逸前:";
echo '<br>$arr[1]='.$arr[1];
echo "<br>";
$arr = $new;
echo "<br>逃逸后:";
echo '<br>$arr[1]='.$arr[1];
echo "<br>";
?>
传入a=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&b=";i:1;s:4:"flag";i:2;s:4:"flag

成功将wtf替换成flag
原理跟上面差不多,这里就不多赘述了
把它原有的字符_吃掉_,再换上我们需要的字符,每一个x都会_吃掉_一个字符
session反序列化
$_SESSION变量直接可控
php引擎的存储格式是键名|serialized_string
php_serialize引擎的存储格式是serialized_string
如果程序使用两个引擎来分别处理的话就会出现问题
代码示例
#1.php
<?php
if(isset($_GET['a'])){
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['nayst'] = $_GET['a'];
var_dump($_SESSION);
}else{
highlight_file(__FILE__);
}
?>
#2.php
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class test{
public $name;
function __wakeup(){
echo $this->name;
}
}
?>
首先访问1.php,传入参数a=|O:4:"test":1:{s:4:"name";s:8:"y4tacker";}
再访问2.php

由于1.php
是使用php_serialize
引擎处理,因此只会把'|'
当做一个正常的字符
然后访问2.php
,由于用的是php
引擎,因此遇到'|'
时会将之看做键名与值的分割符,从而造成了歧义,导致其在解析session文件时直接对'|'
后的值进行反序列化处理
phar反序列化
phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。