学习笔记 · 2021年8月14日 0

序列化与反序列化与反序列化漏洞

一、前言

最近看到个0day,是关于利用某框架的反序列化漏洞,来完成任意命令执行,注入内存马等操作。

想到我之前学习反序列化的时候只是匆匆带过,直到现在我对反序列化漏洞还是不够深入,便写此文进行整理复习

二、什么是序列化

学习反序列化漏洞之前,必须要先了解什么是序列化

百度百科对于序列化的解释是:

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

序列化的开发有以下目的:

1、以某种存储形式使自定义对象持久化

2、将对象从一个地方传递到另一个地方。

3、使程序更具维护性。

通俗的讲,序列化即将对象转化为字节流,便于保存在文件,内存,数据库中;当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要将数据对象等转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复数据对象。发送方将数据对象等转换为字节序列的过程叫做序列化。

举个例子,买一个柜子,从北京运到上海,由于柜子体型大、形状怪异,不方便运输,先把它拆成板子,再装到箱子里,顺丰邮到买家手里,把板子拼装回柜子。这个便是序列化与反序列化的过程。

在Java中:序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。

在PHP中:序列化用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构。

在python中:将变量从内存中变成可存储或者传输的过程称之为序列化。


三、什么是反序列化

将序列化之后形成的可逆的数据结构转化回数据,这就是反序列化。也就是上文中将字节序列再恢复成数据对象 、将板子装回柜子的过程。

以Java为例,假设,我们写了一个class,这个class里面存有一些变量。当这个class被实例化了之后,在使用过程中里面的一些变量值发生了改变。以后在某些时候还会用到这个变量,如果我们让这个class一直不销毁,等着下一次要用它的时候再一次被调用的话,浪费系统资源。

当我们写一个小型的项目可能没有太大的影响,但是随着项目的壮大,一些小问题被放大了之后就会产生很多麻烦。这个时候PHP就和我们说,你可以把这个对象序列化了,存成一个字符串,当你要用的时候再放他出来就好了。

此处便不过多赘述。

四、什么是反序列化漏洞

需要明确的是,序列化与反序列化本身在内部没有漏洞。

漏洞的产生是程序在处理相关序列化与反序列化时,存在用户可控参数,此时用户可以构造恶意代码,如在PHP中, 用户构造恶意序列化代码传入后台,而反序列化会自动调用一些魔术方法,如果魔术方法内存在一些敏感操作例如eval()函数,而且参数是通过反序列化产生的,那么用户就可以通过改变参数来执行敏感操作,这就是反序列化漏洞。

各种语言都有反序列化漏洞,下文以PHP为例做介绍。


1.PHP类与对象

类是定义一系列属性和操作的模板,而对象,就是把属性进行实例化,然后交给类里面的方法,进行处理。

<?php
class man{
   //定义类属性(类似变量),public 代表可见性(公有)
    public $name = 'lyh';
   //定义类方法(类似函数)
   public function example(){
        echo $this->name." is a man...\n";
   }
}

$psycho = new man(); //根据man类实例化对象
$psycho->example();
?>

上述代码定义了一个man类,并在在类中定义了一个public类型的变量$name和类方法example。然后实例化一个对象$psycho,去调用man类里面的example方法,打印出结果。

数据结果:

lyh is a man...

这就是php类与对象最基础的使用。

2.魔术方法

在触发了某个事件之前或之后,魔法函数会自动调用执行,而其他的普通函数必须手动调用才可以执行。PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 __ 为前缀。下表为php常见的魔术方法:

方法名作用
__construct构造函数,在创建对象时候触发,进行初始化对象,一般用于对变量赋初值
__destruct析构函数,和构造函数相反,在对象不再被使用时(将所有该对象的引用设为null)或者程序退出时自动调用
__toString当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串,例如echo打印出对象就会调用此方法
__wakeup()使用unserialize时触发,反序列化恢复对象之前调用该方法
__sleep()使用serialize时触发 ,在对象被序列化前自动调用,该函数需要返回以类成员变量名作为元素的数组(该数组里的元素会影响类成员变量是否被序列化。只有出现在该数组元素里的类成员变量才会被序列化)
__destruct()对象被销毁时触发
__call()在对象中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法
__callStatic()在静态上下文中调用不可访问的方法时触发
__get()读取不可访问的属性的值时会被调用(不可访问包括私有属性,或者没有初始化的属性)
__set()在给不可访问属性赋值时,即在调用私有属性的时候会自动执行
__isset()当对不可访问属性调用isset()或empty()时触发
__unset()当对不可访问属性调用unset()时触发
__invoke()当脚本尝试将对象调用为函数时触发
部分魔法函数

额外提一下__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.PHP序列化与反序列化

在开发的过程中常常遇到需要把对象或者数组进行序列号存储,反序列化输出的情况。特别是当需要把数组存储到mysql数据库中时,我们时常需要将数组进行序列号操作。

php序列化(serialize):是将变量转换为可保存或传输的字符串的过程

php反序列化(unserialize):就是在适当的时候把这个字符串再转化成原来的变量使用

这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。

常见的php系列化和反系列化方式主要有:serialize,unserialize;json_encode,json_decode。

  • 序列化实例
<?php
class porject{
    public $team = 'lyh';                   //声明共有类型性变量$team
    private $team_name = '233';             //声明受保护类型变量$steam_name
    protected $team_group = '19171002';     //声明私有类型变量$team_group
}

$porject = new porject();                   //根据object类实例化对象
echo serialize($porject);                   //进行序列化并输出
?>

输出结果:O:7:”porject”:3:{s:4:”team”;s:3:”lyh”;s:18:”porjectteam_name”;s:3:”233″;s:13:”*team_group”;s:8:”19171002″;}

序列化输出结果

以上是序列化之后的结果,o代表是一个对象,6是对象object的长度,3的意思是有三个类属性,后面花括号里的是类属性的内容,s表示的是类属性team的类型,4表示类属性team的长度,后面的以此类推。值得一提的是,类方法并不会参与到实例化里面。

需要注意的是变量受到不同修饰符(public,private,protected)修饰进行序列化时,序列化后变量的长度和名称会发生变化。

使用public修饰进行序列化后,变量$team的长度为4,正常输出。

使用private修饰进行序列化后,会在变量$team_name前面加上类的名称,在这里是object,并且长度会比正常大小多2个字节,也就是9+6+2=17。

使用protected修饰进行序列化后,会在变量$team_group前面加上*,并且长度会比正常大小多3个字节,也就是10+3=13。

通过对比发现,在受保护的成员前都多了两个字节,受保护的成员在序列化时规则:

1. 受Private修饰的私有成员,序列化时: \x00 +  [私有成员所在类名]  + \x00 [变量名]

2. 受Protected修饰的成员,序列化时:\x00 + * + \x00 + [变量名]

其中,”\x00″代表ASCII为0的值,即空字节,” * ” 必不可少。

序列化格式中的字母含义:

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函数将数据储存到数据库的时候遇到了报错:
//Notice: unserialize(): Error at offset...,
//后来发现serialize()函数对在不同编码下的处理结果是不一样的,序列化后,将编码格式转为UTF-8后使字节数改变,导致了反序列化的时候判断字符长度出现了问题,所以需要使用正则表达式将序列化的数组中的表示字符长度的值重新计算一遍
function mb_unserialize($str) {
    return preg_replace_callback('#s:(\d+):"(.*?)";#s',function($match){return 's:'.strlen($match[2]).':"'.$match[2].'";';},$str);
}
$ser = 'O:7:"porject":3:{s:4:"team";s:3:"lyh";s:18:"porjectteam_name";s:3:"233";s:13:"*team_group";s:8:"19171002";}';
$unser = unserialize(mb_unserialize($ser)); //调用正则表达式函数重新计算并进行反序列化操作
var_dump($unser);                           //使用var_dump输出结果
?>

输出结果:

object(__PHP_Incomplete_Class)[1]
  public '__PHP_Incomplete_Class_Name' => string 'porject' (length=7)
  public 'team' => string 'lyh' (length=3)
  public 'porjectteam_name' => string '233' (length=3)
  public '*team_group' => string '19171002' (length=8)

反序列化输出结果

4.PHP反序列化漏洞

在反序列化过程中,其功能就类似于复原了一个对象,并赋予其相应的属性值。如果让攻击者操纵任意将被反序列化的数据, 那么攻击者就可以实现任意类对象的创建,如果一些类存在一些自动触发的方法(魔术方法),那么就有可能以此为跳板进而攻击系统应用。

挖掘反序列化漏洞的条件是:

  1. 代码中有可利用的类,并且类中有__wakeup(),__sleep(),__destruct()这类特殊条件下可以自己调用的魔术方法。

  2. unserialize()函数的参数可控。

  • 例题一
<?php
class A{
    var $test = "demo";
    function __destruct(){        //创建一个析构函数__destruct()
        @eval($this->test);
<?php
class A{
    var $test = "demo";           //赋初值"demo"                 5.初值"demo"被传入的test参数覆盖
    function __destruct(){        //创建一个析构函数__destruct() 6.进行反序列化时被调用
        @eval($this->test);       //                             7.调用eval执行test中恶意传入的命令
    }
}
$test = $_GET['test'];                                         //1.接收传入参数
$len = strlen($test)+1;                                        //2.计算长度
$p = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}";  //3.填入参数,构造序列化对象payload
$test_unser = unserialize($p);                                 //4.反序列化同时触发_destruct函数
?>

如上代码,最终的目的是通过调用__destruct()这个析构函数,将恶意的payload注入,导致代码执行。根据上面的魔术方法的介绍,当程序执行到unserialize()反序列化的时候,会触发__destruct()方法,同时也可以触发__wakeup()方法。但是如果想注入恶意payload,还需要对$test的值进行覆盖,题目中已经给出了序列化链,很明显是对类A的$test变量进行覆盖。

通过GET构造url传入参数 ?test=phpinfo()

可以看到当我们传入的参数为 phpinfo(),这样的话在调用__destruct方法执行eval()之前就把变量$test的值替换成恶意payload,再通过eval()函数,使phpinfo()执行

  • 例题二
<?php
highlight_file(__FILE__);
error_reporting(0);
class convent{
    var $warn = "No hacker.";
    function __destruct(){
        eval($this->warn);
    }
    function __wakeup(){
        foreach(get_object_vars($this) as $k => $v) {
            $this->$k = null;
        }
    }
}
$cmd = $_POST[cmd];
unserialize($cmd);
?>

这是一题某CTF的赛题,从代码中可以看出类convent,用了两种方法,分别是__ destruct() 、 __wakeup()

1.__destruct: 对象操作执行完毕后自动执行该函数内的代码;
2.__wakeup: 遇到 unserialize 时触发。

这边的 __wakeup 是事件型的,如果没遇到 unserialize 就永远不会触发了,所以我们得先搞清楚先执行哪个方法,再执行哪个方法。借鉴一片代码进行测试,如下:

<?php
header("Content-type: text/html; charset=utf-8");
class people
{
    public $name = "danche";
    public $age = '18';
    function __wakeup(){
        echo "这是 __wakeup()";
        echo "<br>";
    }
    function __construct(){
        echo "这是 __consrtuct()";
        echo "<br>";
    }
    function __destruct(){
        echo "这是 __destruct()";
        echo "<br>";
    }
    function __toString(){
        echo "这是 __toString";
        echo "<br>";
    }
}
$class =  new people();
$class_ser = serialize($class);  //序列化 
print_r($class_ser);            
$class_unser = unserialize($class_ser); //反序列化
print_r($class_unser);
?>

输出结果:

可以看出__consrtuct优先级是最高的,然后执行__wakeup,然后才是__destruc

此时再看上题可以发现,因为遇到了 unserialize()必须先执行__weakup,才能执行到包含eval()函数的__ destruc

题目中 __weakup方法中get_object_vars()函数的作用是返回类中所有的非静态方法,在此题中就是读取出我们将要传入的参数, ,(参考此连接对get_object_vars()函数的解释,此处不多赘述),再通过$this->$k = null清除传入的参数,也就达不到让后面 __ destruc中的eval()执行我们命令的目的,所以必须要绕过 __weakup

__wakeup是当反序列化成功时,才会调用,那我们让它失败呢?只要让它失败,就不会调用这个魔术方法了。payload里面的值是不可以修改的,但是可以修改的属性(变量)数大于实际的个数时,令反序列化失败,就可以绕过 __wakeup

修改源码输出正常的序列化后的字符串:

O:7:"convent":1:{s:4:"warn";s:10:"No hacker.";}

构造payload:

O:7:"convent":2:{s:4:"warn";s:10:"phpinfo();";}

也就是把convent后的1改为其他数字就可以了,也可以改成10。然后把 No hacker.改为 phpinfo();但是要注意修改”No hacker.”前面 s的值,因为 phpinfo();也是占了十位,所以此处不用去改。

post传参里传入题目:

成功利用 __destruct()中的eval执行phpinfo();

五、总结

从上面的文章中我们可以看到序列化就是把对象转换成一种数据格式,如Json、XML等文本格式或二进制字节流格式,便于保存在内存、文件、数据库中或者在网络通信中进行传输。反序列化是序列化的逆过程,即由保存的文本格式或字节流格式还原成对象。

很多编程语言都提供了这一功能,但不幸的是,如果应用代码允许接受不可信的序列化数据,在进行反序列化操作时,可能会产生反序列化漏洞,黑客可以利用它进行拒绝服务攻击、访问控制攻击和远程命令执行攻击。危害十分严重。


参考资料:

https://baike.baidu.com/

https://mp.weixin.qq.com/s/JzGDyP6RGZ4xCxV4gqM2Sw

https://www.cnblogs.com/lcxblogs/p/13539535.html

https://www.cnblogs.com/bmjoker/p/13742666.html

如有遗漏,欢迎作者联系我进行补充

文章若有出错或不理解的,欢迎评论或联系我进行指正交流,转载请标明出处,谢谢