声明:请勿利用文章内的相关技术从事非法测试,如因此产生的一切不良后果与文章作者和本公众号无关。
一.基础信息
一个月前发了一个tp5的鸡肋漏洞,限制必须只能windows系统,这里的tp3就不限制系统,但是还是存在一定的鸡肋,具体看下面文章吧,在7月份写文章的时候那天晚上喝酒了,可能会有错别字或者不通顺的地方,哈哈也是文化水平有限,过一两个月我会发禅道的前台rce,本想着不公开的但是在朋友那块得知貌似hw的时候见过眼熟这里我也不知真假,最近也一直在学车考驾照真是难死我了,希望在年前能拿到驾照。 time 7.15 测试版本Version: 3.2.5 php: 7.3.4 影响范围:<= 3.2.5 漏洞利用poc: index.php?m=Home&c=index&a=../../../../../文件名 调用堆栈: |
File.class.php:88, ThinkStorageDriverFile->load()
Storage.class.php:41, call_user_func_array:{D:phpStudyWWWthinkphp-3.2.5ThinkPHPLibraryThinkStorage.class.php:41}()
Storage.class.php:41, ThinkStorage::__callStatic()
Template.class.php:91, ThinkStorage::load()
Template.class.php:91, ThinkTemplate->fetch()
ParseTemplateBehavior.class.php:38, BehaviorParseTemplateBehavior->run()
Hook.class.php:131, ThinkHook::exec()
Hook.class.php:99, ThinkHook::listen()
View.class.php:155, ThinkView->fetch()
View.class.php:77, ThinkView->display()
Controller.class.php:62, HomeControllerIndexController->display()
Controller.class.php:185, HomeControllerIndexController->__call()
App.class.php:118, ReflectionMethod->invokeArgs()
App.class.php:118, ThinkApp::exec()
App.class.php:214, ThinkApp::run()
Think.class.php:139, ThinkThink::start()
ThinkPHP.php:100, require()
index.php:26, {main}()
二.审计过程
1.1: 路由分析
在Thinkphp5中使用使用最多的就两种方式,但在Thinkphp3中而是三种:
http://127.0.0.1/index.php?m=模块&c=控制器&a=方法 http://127.0.0.1/index.php?s=模块/控制器/方法 上面是可以在文档中查到的,下面分析在代码中的实现。 |
常量的话没遇危险点不用看,ThinkPHP.php又定义了许多常量,在最后面执行了Think::start(), 给进去看看弄了啥,这里面也就是加载了一些配置文件都是写死的没法利用,在这个方法最后面执行了App下面的run方法,但是这块存在三个App类具体是哪一个,可以看到下面61行的时候存在一个inculde 其实下面有好几个但是大致看一下注释就知道是这了,print_r($mode['core']); 发现执行的是 ThinkPHP/Library/Think/App.class.php。 |
跟进去看看干了什么,虽然上面存在很多inculde但是没有一个我们能控制的,所以接着看,下面就是执行到了run() |
public static function run()
{
// 加载动态应用公共文件和配置
load_ext_file(COMMON_PATH);
// 应用初始化标签
Hook::listen('app_init');
App::init();
// 应用开始标签
Hook::listen('app_begin');
// Session初始化
if (!IS_CLI) {
session(C('SESSION_OPTIONS'));
}
// 记录应用初始化时间
G('initTime');
App::exec();
// 应用结束标签
Hook::listen('app_end');
return;
}
其中load_ext_file(COMMON_PATH); COMMON_PATH是/Application/Common/ ,load_ext_file
方法我看了一下其实啥也没干看下图C('LOAD_EXT_FILE')是空的导致两个条件都进不去。
Hook::listen('app_init');也差不多,对于审计来说确实意义不大。
重点:App::init();看下面代码
刚开始写了一些常量$_SERVER['REQUEST_TIME'] 时间戳,$_SERVER['REQUEST_METHOD']请求类型,目前看的话没啥东西,直接看Dispatcher::dispatch();但是Dispatcher类又存在三个类和同样的方法具体是哪一个,可以看找App类的图这块我就不在截图了哈。 |
public static function init()
{
// 日志目录转换为绝对路径 默认情况下存储到公共模块下面
C('LOG_PATH', realpath(LOG_PATH) . '/Common/');
// 定义当前请求的系统常量
define('NOW_TIME', $_SERVER['REQUEST_TIME']);
define('REQUEST_METHOD', $_SERVER['REQUEST_METHOD']);
define('IS_GET', REQUEST_METHOD == 'GET' ? true : false);
define('IS_POST', REQUEST_METHOD == 'POST' ? true : false);
define('IS_PUT', REQUEST_METHOD == 'PUT' ? true : false);
define('IS_DELETE', REQUEST_METHOD == 'DELETE' ? true : false);
// URL调度
Dispatcher::dispatch();
if (C('REQUEST_VARS_FILTER')) {
// 全局安全过滤
array_walk_recursive($_GET, 'think_filter');
array_walk_recursive($_POST, 'think_filter');
array_walk_recursive($_REQUEST, 'think_filter');
}
// URL调度结束标签
Hook::listen('url_dispatch');
define('IS_AJAX', ((isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') || !empty($_POST[C('VAR_AJAX_SUBMIT')]) || !empty($_GET[C('VAR_AJAX_SUBMIT')])) ? true : false);
// TMPL_EXCEPTION_FILE 改为绝对地址
C('TMPL_EXCEPTION_FILE', realpath(C('TMPL_EXCEPTION_FILE')));
return;
}
看到这呢根据注释已经可以看出找到第二种方式了,$varPath输出一下其实就是s,然后我们一直看到 145行的时候执行了define('MODULE_NAME', self::getModule($paths));其中$paths给到MODULE_NAME这个常量,跟进去看看,为啥看这个方法因为我在这个下面看到一个if用到了这个常量可以看下图。 |
第一个if输出一下就知道了,第2个$paths本身就是空,第三个一样的,所以他只能走到else,$var输出是m,$module获取了GET的参数在进行了销毁,第四个直接过,第五个C('URL_MODULE_MAP')是空的,最后直接return返回$module了,然后返回到Dispatcher类,is_dir(APP_PATH . MODULE_NAME) MODULE_NAME目前是我们能控制的,所以必须走下去不然程序直接就结束了,APP_PATH是Application目录这个目录下面有模块Home,其实大致也就清楚了这是第一种路由,走进去之后又是一堆include但是没有能控制的,唯独load_ext_file(MODULE_PATH);可以控制但是上面说过load_ext_file这个方法啥也没干,走到218行和219行进去之后也验证了我们的猜想,代码我就不解释了和模块代码大差不差,下面是图。 |
之后就是执行了一些对本次漏洞没太大意义的东西就不说了,回到init后面也是没啥意义的东西。 |
1.2:漏洞执行过程
前面分析了路由下面我们看看这次的文件包含如何产生的,回到run()方法,215行执行了App::exec();跟进去看看,CONTROLLER_NAME我么再熟悉不过了不就是刚才c的常量吗 /^[A-Za-z](/|w)*$/ 必须是这个正则的内容 不是的话$module是false,就会执行到95行 exit就结束了,所以我们正常输入其中几个if大家输出一下就知道了我就不废话了,会走到else里面 这个方法不就是控制器的意思吗跟进去看看controller。 |
function controller($name, $path = '')
{
$layer = C('DEFAULT_C_LAYER');
if (!C('APP_USE_NAMESPACE')) {
$class = parse_name($name, 1) . $layer;
import(MODULE_NAME . '/' . $layer . '/' . $class);
} else {
$class = ($path ? basename(ADDON_PATH) . '\' . $path : MODULE_NAME) . '\' . $layer;
$array = explode('/', $name);
foreach ($array as $name) {
$class .= '\' . parse_name($name, 1);
}
$class .= $layer;
}
if (class_exists($class)) {
return new $class();
} else {
return false;
}
}
$class = ($path ? basename(ADDON_PATH) . '\'. $path : MODULE_NAME) . '\' . $layer;
$array = explode('/', $name);
foreach ($array as $name) {
$class .= '\' . parse_name($name, 1);
}
$class .= $layer;
}
if (class_exists($class)) {
return new $class();
上面的是重点,其他的不用看,MODULE_NAME常量是m $layer是controller $name就是刚开始传过来的常量 CONTROLLER_NAME也就是c
(class_exists($class)查看我们输出的类存不存在,存在直接new不存在就false。
下面就是109行ACTION_NAME是a,给到$action,113行进去看看,里面其实做了一个反射,主要看
117行通过反射动态执行了一个__call魔术方法,但是要执行到116行必须要存在一个错误,所以还是要进去看看113行。
$action是我们能控制的a,只要不是这个正则里面的就能执行到116行了$module也就是我们的类。 |
那么Application/Home/Controller/IndexController.class.php并没有__call这个方法呀?难道必须我们自己写吗?那这叫什么狗屁漏洞,别急IndexController类确实没有但是默认会继承一个父类Controller类。 |
可以看到确实存在这个魔术方法,$method->invokeArgs($module, array($action, '')); array($action, '')其中$action是我们可以控制的也就是a,function __call($method, $args) $method也就是$action。 等等我在__call看到了什么$this->display();搜嘎我相信到这里就清楚了吧,第一个if输出一下就知道了,第2个method_exists判断当前类里面存在不存在_empty,也可以过了因为默认没有,parseTemplate跟进去看看呗。 |
if ('' == $template) {
// 如果模板文件名为空 按照默认规则定位
$template = CONTROLLER_NAME . $depr . ACTION_NAME;
} elseif (false === strpos($template, $depr)) {
$template = CONTROLLER_NAME . $depr . $template;
}
$file = THEME_PATH . $template . C('TMPL_TEMPLATE_SUFFIX');
if (C('TMPL_LOAD_DEFAULTTHEME') && THEME_NAME != C('DEFAULT_THEME') && !is_file($file)) {
// 找不到当前主题模板的时候定位默认主题中的模板
$file = dirname(THEME_PATH) . '/' . C('DEFAULT_THEME') . '/' . $template . C('TMPL_TEMPLATE_SUFFIX');
}
return $file;
这个是漏洞产生的原因,也是执行到$this->display();造成文件包含的原因,因为$this->display();里面也执行了parseTemplate方法。
$template本来就是空的,ACTION_NAME常量是我们一路看过来的东西就是a,C('TMPL_TEMPLATE_SUFFIX')输出了一下是html的,然后返回了$file。
然后还有一个file_exists_case方法我就不解释了下面是图就是看文件存在不存在。 |
最后执行了$this->display();跟进去。 |
继续呗。 |
看看fetch,121行和我上面说的一样就不进去看了,$templateFile是我们可以控制的,到了154行给了一个数组里面$params,155行进去看看对数组做了什么。 |
接着看看self::exec |
不知道的直接输出一下。
当前类的run方法。
Template类的fetch |
接着进入loadTemplate这块我就不具体说了可以看Thinkphp3变量覆盖导致rce的文章,简单来说就是把文件进行读取出来130把内容进行过滤(这块的过滤可以忽略),然后创建缓存文件,131行把内容写入到缓存文件里面,最后返回这个缓存文件的路径之后执行Storage::load。 |
Storage::load跟进去看看 |
到这也就是成功进行文件包含了但是存在条件就是,上面我们分析到C('TMPL_TEMPLATE_SUFFIX') 是html所以只能包含html的文件。 |
三.漏洞利用
首先在根目录下面创建一个文件名字随便但是必须是html扩展名,内容为php的代码,就phpinfo吧poc: http://127.0.0.1/index.php?m=Home&c=index&a=../../../../../2 |
Thinkphp3已经不再更新维护了,但是有很多优秀项目依然在使用tp3,这里就简单举个例子,在我写这篇文章的时候showdoc-3.2.6目前最新版本一下都存在这个漏洞。 |
四:GETshell
还在尝试中......
原文始发于微信公众号(摸鱼Sec):Thinkphp3文件包含(CNVD-2024-39045)
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论