骑士cms存在模板解析漏洞
漏洞简介
前段时间,骑士cms 发布了紧急风险漏洞升级通知,网上也有不少分析的文章,骑士cms 利用了 Thinkphp3.2.3 的框架,所以就想分析一下这个漏洞,对 Thinkphp3.2.3 的框架有一个初步的了解。
漏洞复现
我们选用
74cms v6.0.20作为复现的版本,选取 phpstudy 自带的 apache + mysql 环境来进行安装。
根据官网发布的信息,我们大致可以判断出漏洞存在的位置是
\Common\Controller\BaseController::assign_resume_tpl
我们在函数内部加入断点,根据函数存在位置,属于 Common 模块下的 Base 控制器中的assign_resume_tpl ,构造路由
http://74cms.test/index.php?m=common&c=base&a=assign_resume_tpl&variable=1&tpl=2
并不能直接进入断点位置
然后我就直接搜索了 错误信息 [ WE CAN DO IT JUST THINK ]
通过一步一步向上寻找错误触发的位置,发现是无法加载模块:Common
\Think\Think::halt
\Think\Think::appException
ThinkPHP/Library/Think/Dispatcher.class.php
最后发现在加载模块时会检测模块名,无法加载 Common 模块。Common 模块无法直接调用,所以我们需要找其他方法调用 Common 模块下的 Base 控制器中的assign_resume_tpl 方法。
我们注意到 Home 模块下的 IndexController 继承 FrontendController
FrontendController 继承 BaseController
所以我们通过构造路由
http://74cms.test/index.php?m=home&c=index&a=assign_resume_tpl&variable=1&tpl=2
在第一次进行调试分析时,在关键地方加上断点,死活都进入不到断点定位的位置,但是相关功能已经执行,然后我发现会跟进一个文件 common~runtime.php
这个文件中乱七八糟的,但是里面的内容又好像是代码,通过查阅资料发现ThinkPHP的编译缓存文件~runtime.php
~runtime.php 中缓存的编译内容,相当于把 index.php 引导的所有操作全部集成到 ~runtime.php 文件中。有了这个缓存的编译文件,index.php 在下次运行时,不再引导,而是直接检测是否存在 ~runtime.php 编译缓存文件,如果在,则直接运行 ~runtime.php。
我就将文件夹下的 common~runtime.php 删除,就实现了调试自由。
通过包含日志实现命令执行
1 | http://74cms.test/index.php?m=home&c=index&a=assign_resume_tpl |
data/Runtime/Logs/Home/20_12_17.log
1 | http://74cms.test/index.php?m=home&c=index&a=assign_resume_tpl |
通过包含图片实现命令执行
bmp 图片
1 | #define test_width 16 |
注册普通用户完善自己的信息时,在上传照片/作品处上传构造的恶意文件
查看上传成功的文件位置
1 | http://74cms.test/index.php?m=home&c=index&a=assign_resume_tpl |
在本地利用的是 windows 环境下的 phpstudy-php5.5.9,水泡泡师傅指点说,这个 trick 仅仅适用于 phpstudy 特定的 php 版本中才可以利用成功。
漏洞分析
通过包含日志实现命令执行
首先我们先在根目录下的 data 文件夹中放一个文件,尝试进行包含
构造payload http://74cms.test/index.php?m=home&c=index&a=assign_resume_tpl&variable=1&tpl=data/a
\Common\Controller\BaseController::assign_resume_tpl
在函数内部中调用了 fetch 方法,控制器中没有 fetch 方法 ,则继承自父类的 fetch 方法。 此时传递的 $tpl 是要被解析的模板的路径。
在父类中,看到进一步调用了 实例化 view 对象中的 fetch 方法。
\Think\Controller::fetch
\Think\Controller::__construct
跟进 \Think\View::fetch
,此时 fetch 函数传递的三个参数,$templateFile 对应的是要被解析的模板的路径,$content 和 $prefix 的值为空。
\Think\View::fetch
因为 $content 的值为空 所以跟进函数 parseTemplate
\Think\View::parseTemplate
在这个地方使用 is_file 函数判断传递的模板是否是个文件,如果文件存在且为正常的文件,则返回 true。
接下来回到函数 \Think\View::fetch
会进行 对 TMPL_ENGINE_TYPE
值的判断,通过查看 ThinkPHP/Conf/convention.php
可以看到 'TMPL_ENGINE_TYPE' => 'Think',
所以最后执行到了 Hook::listen('view_parse',$params);
。
\Think\Hook::listen
这个 Hook 类是一个行为扩展,在 thinkphp3.2 中称之为钩子,当我们传递一个 “view_parse” 的参数之后,实际上是触发了一个 “view_parse” 事件,在 Hook::listen 方法中,查找 $tags 变量中有没有绑定 “view_parse” 方法,然后用 foreach 遍历 $tags 属性,并执行 Hook:exec 方法。
在传递过程中注意到 “view_parse” 绑定了 Behavior\ParseTemplateBehavior
ThinkPHP/Mode/common.php
\Think\Hook::exec
在 Hook::exec 会进行判断,当其中含有 Behavior
时,其入口方法为 run
\Behavior\ParseTemplateBehavior::run
模板的引擎是 “think” 所以跟进第一个判断,如果是第一次解析 content 为空,进入 else,先实例化 template 类,然后再调用 fetch 方法。此时传入的参数 $_content 的值为 $data[‘file’] ,是解析模板的路径。
\Think\Template::fetch
在 \Think\Template::fetch
中调用 loadTemplate 来对解析的模板文件进行解析和编译
\Think\Template::loadTemplate
在 loadTemplate 中 首先读取了要解析的模板文件,将其保存在了 $tmplContent 中,然后利用 cpmplier 对模板文件进行编译
\Think\Template::compiler
在 complier 中将模板文件的内容直接拼接到 $tmplContent,对代码进行优化之后,直接返回代码。
又回到函数 \Think\Template::loadTemplate
将经过函数 complier 编译后的文件进行存储,最后返回存储的路径
data/Runtime/Cache/Home/1e84025731a9331f59f3a61078fe4420.php
再回到函数 \Think\Template::fetch
经过 loadTemplate 函数的处理,最后得到的是解析编译后模板文件的路径
\Think\Template::fetch
在 fetch 中调用 load 方法加载模板
\Think\Storage\Driver\File::load
在load 函数中进行了非空判断之后,直接调用了 include 去包含传入的文件地址
但是直接利用之前的 payload 在页面上显示为空,并没有直接回显出 phpinfo 的信息,这是因为在函数 \Think\View::fetch
中
在进行模板解析和包含之前 通过 ob_start 打开缓冲区, phpinfo 输出的信息被存储在缓冲区内, Hook::listen('view_parse',$params);
代码执行之后,又通过ob_get_clean() 获取并清空了缓存,因此虽然 phpinfo 执行成功,但是在页面上没有回显。
可以通过如下方法实现在页面上的回显
<?php phpinfo(); ob_flush();?>
// ob_flush 输出缓冲区中的内容<?php phpinfo(); die();?>
文件包含的流程图
知道文件包含的流程,现在就是要分析一下如何实现对将模板文件的上传,首先一处就是通过日志来实现
\Think\View::fetch
恰好在 fetch 函数中 首先判断了是否存在请求的模板文件,如果不存在模板文件就会将错误信息,写入日志文件中
data/Runtime/Logs/Home/20_12_22.log
然后再去包含日志文件就可以了。 为什么要通过 post 请求去生成日志,因为通过 get 请求中的 url 会在日志文件中被 url 编码, post 请求则不会。 照着网上的分析说的,但是好像不是很对,通过 get 方法也是可以成功的,建议自己进行尝试?
拓展思考
绕过骑士cms 补丁
我们关注一下骑士cms 的补丁信息
\Common\Controller\BaseController::assign_resume_tpl
ThinkPHP/Library/Think/View.class.php
仅仅是过滤了报错日志文件的生成,对文件包含漏洞并没进行修复(文件包含漏洞似乎是Thinkphp 3.2.3 的原生漏洞),这样的话,通过上传恶意文件,然后再实现文件包含,仍然能够造成命令执行。
这个是受局限的 bmp 图片文件,要进一步实现利用,还是需要进一步分析一下绕过 php_gd 的方法。
Thinkphp 3.2.3 原生问题
所有漏洞的触发过程全部实现在 thinkphp3 内部框架之内,是因为进行模板解析之前没有控制传入模板的路径,解析的过程中也没有过滤模板内文件的内容,解析编译之后直接通过 include 方式将模板文件进行了包含。
我们在 Home 模块下添加一个 Whippet 控制器
http://74cms.test/index.php?m=home&c=whippet&a=index&variable=1&tpl=<?php phpinfo(); ob_flush();?>
http://74cms.test/index.php?m=home&c=whippet&a=index&variable=1&tpl=data/Runtime/Logs/Home/20_12_22.log
可以注意到在脱离了骑士cms的文件内容之后,利用自写的模块也能成功触发漏洞,整个漏洞触发的过程与之前完全一致。这个漏洞其实本质上就是 thinkphp3框架中本身的问题,如果有程序基于 thinkphp3 的框架,并且调用了 Contolller 控制器中的 fetch 方法,模板的路径自定义,就可以触发这个漏洞。
Thinkcmf 的任意文件包含
大致看了一下 ThinkCmf 中的任意文件包含漏洞,没有调试,但是发现大致原理是相同的,最后触发的位置也相同。/具体不做分析了/,而且发现,thinkcmf 上的 payload 放在 骑士cms 也是可行的,有时间还是再看一下 thinkcmf 看一下是否存在新的问题。还是有一些不同的,不能一概而论。
http://74cms.test/index.php?m=home&c=whippet&a=index&variable=1&tpl=<?php file_put_contents("shell.php","<?php phpinfo();?>");?>
http://74cms.test/index.php?m=home&c=whippet&a=index&variable=1&tpl=data/Runtime/Logs/Home/20_12_22.log
参考文章
骑士cms < 6.0.48任意文件包含漏洞简记
骑士cms从任意文件包含到远程代码执行漏洞分析
骑士 CMS 远程命令执行分析
这个漏洞,我劝你耗子尾汁