PbootCMS2.0.7前台任意文件包含漏洞复现

[toc]

前言

之前看到网上有关于这个漏洞分析的文章,这两天没什么事情做就想着自己也看一看,分析漏洞的成因,复现一下。
先在github上面下载好对应版本的源码,phpstudy一把梭。

漏洞分析

触发文件包含的地方在 core\view\View.php 文件的view类中,直接看这个方法吧

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
public function parser($file)
{
// 设置主题
$theme = isset($this->vars['theme']) ? $this->vars['theme'] : 'default';

$theme = preg_replace('/\.\.(\/|\\\)/', '', $theme); // 过滤掉相对路径
$file = preg_replace('/\.\.(\/|\\\)/', '', $file); // 过滤掉相对路径

if (strpos($file, '/') === 0) { // 绝对路径模板
$tpl_file = ROOT_PATH . $file;
} elseif (! ! $pos = strpos($file, '@')) { // 跨模块调用
$path = APP_PATH . '/' . substr($file, 0, $pos) . '/view/' . $theme;
define('APP_THEME_DIR', str_replace(DOC_PATH, '', $path));
if (! is_dir($path)) { // 检查主题是否存在
error('模板主题目录不存在!主题路径:' . $path);
} else {
$this->tplPath = $path;
}
$tpl_file = $path . '/' . substr($file, $pos + 1);
} else {
// 定义当前应用主题目录
define('APP_THEME_DIR', str_replace(DOC_PATH, '', APP_VIEW_PATH) . '/' . $theme);
if (! is_dir($this->tplPath .= '/' . $theme)) { // 检查主题是否存在
error('模板主题目录不存在!主题路径:' . APP_THEME_DIR);
}
$tpl_file = $this->tplPath . '/' . $file; // 模板文件
}
$note = Config::get('tpl_html_dir') ? '<br>同时检测到您系统中启用了模板子目录' . Config::get('tpl_html_dir') . ',请核对是否是此原因导致!' : '';
file_exists($tpl_file) ?: error('模板文件' . APP_THEME_DIR . '/' . $file . '不存在!' . $note);
$tpl_c_file = $this->tplcPath . '/' . md5($tpl_file) . '.php'; // 编译文件

// 当编译文件不存在,或者模板文件修改过,则重新生成编译文件
if (! file_exists($tpl_c_file) || filemtime($tpl_c_file) < filemtime($tpl_file) || ! Config::get('tpl_parser_cache')) {
$content = Parser::compile($this->tplPath, $tpl_file); // 解析模板
file_put_contents($tpl_c_file, $content) ?: error('编译文件' . $tpl_c_file . '生成出错!请检查目录是否有可写权限!'); // 写入编译文件
$compile = true;
}

ob_start(); // 开启缓冲区,引入编译文件
$rs = include $tpl_c_file;
if (! isset($compile)) {
foreach ($rs as $value) { // 检查包含文件是否更新,其中一个包含文件不存在或修改则重新解析模板
if (! file_exists($value) || filemtime($tpl_c_file) < filemtime($value) || ! Config::get('tpl_parser_cache')) {
$content = Parser::compile($this->tplPath, $tpl_file); // 解析模板
file_put_contents($tpl_c_file, $content) ?: error('编译文件' . $tpl_c_file . '生成出错!请检查目录是否有可写权限!'); // 写入编译文件
ob_clean();
include $tpl_c_file;
break;
}
}
}
$content = ob_get_contents();
ob_end_clean();
return $content;
}

漏洞触发的原因是这里对危险字符的过滤不规范导致,可以直接双写绕过

往下看

当模板文件不在缓存中的时候,会读取$tpl_file中的内容,然后写入缓存文件中并且包含。
也就是说,当parser函数的参数可以被控制的时候,就会造成一个任意文件包含。
我们要寻找一个参数可控的parser的点,然后可以在前台控制器 TagController 的index()中找到

这里调用了parser(),参数$tagstpl来自 request() 方法,这里的大概意思就是需要POST提交一个名为tagstpl的变量,就不跟过去看了。
$tagstpl还会拼接$this->htmldir,但是我们可以穿越目录,怎么拼接也无所谓了。
那这么说我们只需要post提交我们的payload就可以包含任意文件了。

读取一下根目录下的phpinfo.php

成功了。

漏洞修复

官方在2.0.8版本就即时做出了修复,看看官方是怎么修复的


用自定义的 preg_replace_r() 函数代替了原本的 preg_replace() ,递归替换

这里使$tagstpl强制为html后缀,无法读取任意文件了。