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
代码审计
<?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(类):
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:
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
所以我们这里需要知道,用三个等号时,除了两个变量的值相同外,还必须这两个变量的类型相同,而用两个等号时,只需要两个变量值相同。
我们构造payload时,构造op=‘ 2’字符串,则op=‘2’就不成立,此时op就成了我们自己设置的值。然后通过process()函数调用后:
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为:
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实例化。