序列化与反序列化

为了有效地存储或传递数据,同时不丢失其类型和结构,经常需要利用序列化和反序列化函数对数据进行处理

序列化函数返回字符串,此字符串包含了表示值的字节流,可以存储于任何地方

反序列化函数对单一的已序列化的变量进行操作,将其转换回原来的值

序列化

PHP中的序列化函数是serialize()

NULL和标量类型数据的序列化

  • NULL

在PHP中,NULL被序列化为N

  • bool型

bool型数据被序列化为b:<digit>。其中,表示0或1

当bool型数据为false时,为0,反之为1

  • int型

int型数据被序列化为i:<number>。其中,为一个整型数,范围为-2147483648~2147483647,数字前可以有正负号

如果被序列化的数字溢出则被序列化为浮点型

如果序列化后的数字超过这个范围,则反序列化时不会返回期望的数值

  • double型

double型数据被序列化为d:<number>,其中,为一个浮点数,其范围与PHP的浮点数范围一样,可以表示为整数形式、浮点数形式和科学计数法形式

如果序列化(负)无穷大,则为(-)INF

如果序列化后的数字小于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>表示对象中的字段个数

这些字段包括在对象所在类及其祖先类中用varpublicprotectedprivate声明的字段,但是不包括用staticconst声明的静态字段,也就是说只有实例字段

<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成员函数(如果存在)

反序列化漏洞

反序列化漏洞产生的主要原因是;

  1. unserialize函数的参数可控
  2. 存在魔法函数

魔法函数

__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

  1. __construct

用法:

void __construct([mixed $args[, $...]])

PHP5允许开发者在一个类中定义一个方法作为构造函数

具有构造函数的类会在每次创建新对象时先调用此方法,所以__construct函数非常适合在使用对象之前做一些初始化工作

  1. __destruct

用法:

void __destruct(void)

PHP5引入了析构函数的概念,这类似于其他面对对象的语言,如C++

__destruct函数是对象被销毁的时候进行调用,通常PHP在程序块执行结束时进行垃圾回收,这将进行对象销毁,然后自动触发__destruct魔术方法

  1. 代码示例
<?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

  1. __sleep

serialize函数会检查类中是否存在__sleep函数,如果存在,该函数会先被调用,然后才执行序列化操作

此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组

如果该函数未返回任何内容,则NULL被序列化,并产生一个E_NOTICE级别的错误

__sleep函数不能返回父类的私有成员的名字,这样做会产生一个E_NOTICE级别的错误,该函数可以用serializable接口来代替

__sleep函数常用于提交未提交的数据或进行类似的清理操作

  1. __wakeup

unserialize函数会检查是否存在__wakeup函数,如果存在,则会先调用__wakeup函数,预先准备对象需要的资源

__wakeup函数常用在反序列化操作中,例如,重新建立数据库连接或执行其他初始化操作

  1. 代码示例
<?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";}

成功将wtf替换成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内的内容。