
一、前言
为了学习Thinkphp框架的运行原理以及加强自身代码审计能力,所以特意在网上寻找了一个由php编写的CMS漏洞文章,对其进行复现和逆向代码审计。漏洞参考文章如下:
调试工具:
phpstudy2018+phpstorm+xdebug; Firefox; xdebug helper
工具使用说明:
(1) phpstudy2018中已经内置了适应的xdebug模块,可以根据需要进行自由调整,较为方便。
(2) phpstudy2018中Xdebug模块位置:phpstudy安装路径\phpstudy\PHPTutorial\php\php-{版本}-nts\ext\php_xdebug.dll
(3) 环境配置参考连接:
(32条消息) ThinkPHP学习笔记(七)--PHPstorm+PHPstudy+Xdebug断点调试_limingliang_的博客-CSDN博客_thinkphp断点调试
二、代码审计的思路
(1)利用PHP代码中文件包含存在的高危函数,快速定位那些文件可能存在本地文件包含漏洞。(主要通过全局搜索功能)
(2)在分析文件是否存在本地含漏洞过程中,分析文件关系,确认文件的主要功能点。(静态跟踪函数和变量)
(3)定位可控参数传参点,并使用动态调试,分析数据之间的传递关系。(动态调试)
三、代码审计过程
1.利用关键函数(include)定位可能存在文件包含漏洞的PHP文件,再经过排除从搜索结果中显示文件名已经固定好的文件,最后结果如下:
core/Lib/View.class.php
core/ThinkPHP/Lib/Think/Core/View.class.php
core/ThinkPHP/Lib/Think/Template/ThinkTemplate.class.php
core/ThinkPHP/Common/functions.php
temp/~runtime.php
2.依次定位上述三个文件中的函数位置,并分析逻辑,结果如下:
Lib\View.class.php
通过分析上述代码发现,该函数在调试时使用,同时由于开头已经定义了包含文件名($traceFile = CONFIG_PATH.'trace.php';),因此不存在可输入点,可以直接跳过。
Note:
(1) 经过追踪变量CONFIG_PATH,发现该变量在(core/ThinkPHP/Common/paths.php)已经定义。
(3) 变量 APP_PATH定位位置:core/Conf/define.php
(3) 变量 CONF_DIR定义的位置:core/ThinkPHP/Common/paths.php
综上所述,同理查找该文件中使用了include其他的函数,最后发现未使用固定的文件路径的函数为:fetch()
代码如下:
public function fetch($templateFile='',$charset='',$contentType='text/html',$display=false)
{
$GLOBALS['_viewStartTime'] = microtime(TRUE);
if(null===$templateFile)
// 使用null参数作为模版名直接返回不做任何输出
return ;
if(empty($charset)) $charset = C('DEFAULT_CHARSET');
// 网页字符编码
header("Content-Type:".$contentType."; charset=".$charset);
header("Cache-control: private"); //支持页面回跳
//页面缓存
ob_start();
ob_implicit_flush(0);
if(!file_exists_case($templateFile))
// 自动定位模板文件
$templateFile = $this->parseTemplateFile($templateFile);
$engine = strtolower(C('TMPL_ENGINE_TYPE'));
if('php'==$engine) {
// 模板阵列变量分解成为独立变量
extract($this->tVar, EXTR_OVERWRITE);
// 直接载入PHP模板
include $templateFile;
}elseif('think'==$engine && $this->checkCache($templateFile)) {
// 如果是Think模板引擎并且缓存有效 分解变量并载入模板缓存
extract($this->tVar, EXTR_OVERWRITE);
//载入模版缓存文件
include C('CACHE_PATH').md5($templateFile).C('TMPL_CACHFILE_SUFFIX');
}else{
// 模板文件需要重新编译 支持第三方模板引擎
// 调用模板引擎解析和输出
$className = 'Template'.ucwords($engine);
require_cache(THINK_PATH.'/Lib/Think/Util/Template/'.$className.'.class.php');
$tpl = new $className;
$tpl->fetch($templateFile,$this->tVar,$charset);
}
$this->templateFile = $templateFile;
// 获取并清空缓存
$content = ob_get_clean();
// 模板内容替换
$content = $this->templateContentReplace($content);
// 布局模板解析
$content = $this->layout($content,$charset,$contentType);
// 输出模板文件
return $this->output($content,$display);
}
通过分析以上代码逻辑,出现以下结论:
(1)该函数存在接收变量:$templateFile
(2)该函数在接收变量的参数值时,不存在严格过滤行为
(3)该函数中存在 include $templateFile 操作
因此可以判断该函数存在本文件包含漏洞,需要进一步跟踪该函数被调用的位置,同时确认 $templateFile 变量是否能够由URL中获取。
经过查找,发现 $templateFile 变量在该文件中被定义,但未找到该项目中,调用该文件下View类的方法。(后续分析发现,是源代码有过改动,因此不能利用。)
3.通过上述结论,并查找项目中所有代码发现,开发者相同的函数存在多处定义的情况。因此直接利用函数 fetch() 查找代码,最终发现如下文件中,也存在相同的函数定义和调用:
(1)temp/~runtime.php
(2)core/ThinkPHP/Lib/Think/Core/Action.class.php
(1)经过分析发现 core/ThinkPHP/Lib/Think/Core/Action.class.php 文件中不存在 fetch() 函数调用,因此直接排除该文件。
(2)分析 temp/~runtime.php 的源代码中的fecth()函数。
public function fetch($templateFile='',$charset='',$contentType='text/html',$display=false) {
$GLOBALS['_viewStartTime'] = microtime(TRUE);
if(null===$templateFile) return ;
if(empty($charset)) $charset = C('DEFAULT_CHARSET');
header("Content-Type:".$contentType."; charset=".$charset);
header("Cache-control: private");
ob_start();
ob_implicit_flush(0);
if(!file_exists_case($templateFile)) $templateFile = $this->parseTemplateFile($templateFile);
$engine = strtolower(C('TMPL_ENGINE_TYPE'));
if('php'==$engine) {
extract($this->tVar, EXTR_OVERWRITE);
include $templateFile;
} elseif('think'==$engine && $this->checkCache($templateFile)) {
extract($this->tVar, EXTR_OVERWRITE);
include C('CACHE_PATH').md5($templateFile).C('TMPL_CACHFILE_SUFFIX');
} else {
$className = 'Template'.ucwords($engine);
require_cache(THINK_PATH.'/Lib/Think/Util/Template/'.$className.'.class.php');
$tpl = new $className;
$tpl->fetch($templateFile,$this->tVar,$charset);
}
$this->templateFile = $templateFile;
$content = ob_get_clean();
$content = $this->templateContentReplace($content);
$content = $this->layout($content,$charset,$contentType);
return $this->output($content,$display);
}
通过分析代码能发现存在两处本地文件包含漏洞位置;
if('php'==$engine) {
extract($this->tVar, EXTR_OVERWRITE);
include $templateFile;
} elseif('think'==$engine && $this->checkCache($templateFile)) {
extract($this->tVar, EXTR_OVERWRITE);
include C('CACHE_PATH').md5($templateFile).C('TMPL_CACHFILE_SUFFIX');
} else {
$className = 'Template'.ucwords($engine);
require_cache(THINK_PATH.'/Lib/Think/Util/Template/'.$className.'.class.php');
$tpl = new $className;
$tpl->fetch($templateFile,$this->tVar,$charset);
}
include $templateFile;
经过分析代码逻辑发现,该处由于自定义变量 TMPL_ENGINE_TYPE=think,不满足 **if('php'==$engine)**判定条件,因此无法调用。
include C('CACHE_PATH').md5($templateFile).C('TMPL_CACHFILE_SUFFIX')
1)通过继续追踪代码逻辑,需要满足调用 checkCache($templateFile) 的条件才能使用include,接下来继续追踪分析分析 checkCache($templateFile)
protected function checkCache($tmplTemplateFile) {
if (!C('TMPL_CACHE_ON')) return false;
$tmplCacheFile = C('CACHE_PATH').md5($tmplTemplateFile).C('TMPL_CACHFILE_SUFFIX');
if(!is_file($tmplCacheFile)) {
return false;
} elseif (filemtime($tmplTemplateFile) > filemtime($tmplCacheFile)) {
return false;
} elseif (C('TMPL_CACHE_TIME') != -1 && time() > filemtime($tmplCacheFile)+C('TMPL_CACHE_TIME')) {
return false;
}
return true;
}
通过最终追踪分析 C('CACHE_PATH')=/temp/Cache和 C('TMPL_CACHFILE_SUFFIX')=html,得到变量 $tmplCacheFile=/temp/Cache/模板名称.html,并判断缓存文件是否存在;若缓存文件不存在,则放回True;若存在缓存文件,且缓存文件的修改时间要小于保存的时间,则返回FALSE。(在进一步的动态调试测试分析过程中,发现TMPL_CACHE_ON 变量经过处理后一直为 OFF状态,因此会直接跳过。)
2)返回fetch()函数,继续分析,最终定位到可利用代码块如下:
a.不存在缓存文件的情况,且 TMPL_CACHE_ON在传入是为 ON 状态:
...
extract($this->tVar, EXTR_OVERWRITE);
include C('CACHE_PATH').md5($templateFile).C('TMPL_CACHFILE_SUFFIX');
...
可以直接通过该值进行利用。(PS:这个时候与ThinkPHP3.X~5.X 存在的本地包含漏洞原理一致)
b.存在缓存文件的情况:
...
$className = 'Template'.ucwords($engine);
require_cache(THINK_PATH.'/Lib/Think/Util/Template/'.$className.'.class.php');
$tpl = new $className;
$tpl->fetch($templateFile,$this->tVar,$charset);
...
进一步跟踪代码,发现是调用并加载模板文件的过程,同时发现调用了 core/ThinkPHP/Lib/Think/Util/Template/TemplateThink.class.php文件中的 **load()**函数。通过进一步跟进函数,发现 load() 函数中存在 文件包含漏洞代码:include $templateCacheFile,至此产生文件包含漏洞的源代码定位成功。
4.定位参数传递的位置
前提说明:
由于通过观察源代码项目结构很容易发现,ekucms2.5是利用了thinkphp的MVC(即:Moudle,View,Controller)模式,其传输的URL参数格式有三种:
(1)http://{IP:port}/index.php?a=test1&b=test2
(2)http://{IP:port}/index.php/{moudle}/{Controller}/变量/变量值
(3)http://{IP:port}/index.php/?s={moudle}/{Controller}/变量/变量值
通过观察发现,其传输参数为第三种,因此只需要寻找到调用 TemplateThink.class.php 文件的模块和控制器即可
(1)首先依据thinkphp的目录结构,寻找配置文件,查看那些模块能够从HTTP报文中获取参数值。通过搜索最终发现 config.php中存在两处-
分别为:/home/info/detail 和 /home/my/show,依次访问相应路径的网页,查看是否正常触发断点。最终确认 /home/my/show模块能够能够向 参数 id传入值。
处理id值的文件位置为:core/Lib/Action/Home/MyAction.class.php
四、总结
ekucms2.5底层采用thinkphp5的框架编写,本次本地文件包含漏洞本质上与thinkphp5.x的产生的原因基本一致。在进行漏洞利用时,程序运行的流程图参考如下:
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)