1.环境:
php5.5.38+apache+seacms v6.45
seacms目录结构:
│─admin //后台管理目录 │ │─coplugins //已停用目录 │ │─ebak //帝国备份王数据备份 │ │─editor //编辑器 │ │─img //后台静态文件 │ │─js //后台js文件 │ │─templets //后台模板文件 │─article //文章内容页 │─articlelist //文章列表页 │─comment //评论 │ │─api //评论接口文件 │ │─images //评论静态文件 │ │─js //评论js文件 │─data //配置数据及缓存文件 │ │─admin //后台配置保存 │ │─cache //缓存 │ │─mark //水印 │ │─sessions //sessions文件 │─detail //视频内容页 │─include //核心文件 │ │─crons //定时任务配置 │ │─data //静态文件 │ │─inc //扩展文件 │ │─webscan //360安全监测模块 │─install //安装模块 │ │─images //安装模块静态文件 │ │─templates //安装模块模板 │─js //js文件 │ │─ads //默认广告目录 │ │─player //播放器目录 │─list //视频列表页 │─news //文章首页 │─pic //静态文件 │ │─faces //表情图像 │ │─member //会员模块界面 │ │─slide //旧版Flash幻灯片 │ │─zt //专题静态文件 │─templets //模板目录 │─topic //专题内容页 │─topiclist //专题列表页 │─uploads //上传文件目录 │─video //视频播放页 │─weixin //微信接口目录 └─index.php //首页文件
2.利用代码
poc1
http://seacms.test/search.php POST: searchtype=5&order=}{end if} {if:1)phpinfo();if(1}{end if}
poc2:
POST: searchtype=5&order=}{end if}{if:1)$_POST[func]($_POST[cmd]);//}{end if}&func=system&cmd=whoami
searchtype=5&order=}{end if}{if:1)$_POST[func]($_POST[cmd]);if(1}{end if}&func=system&cmd=whoami
3.执行效果
4.漏洞分析
漏洞产生链如上图所示,在search.php的212行下断点,因为在此处产生了parseIf()函数的调用,并且最终的命令执行是发生在此函数中,用payload打一次,将停在此处,进入此函数进行分析
如上图所示,其中buildregx函数是构建php的原生正则表达式
接下来使用$labelRule规则进行preg_match_all匹配出了所有满足的结果,并放在$iar中,我猜测这里class顶一个两个css样式,通过if条件来调整按钮样式的
通过这4行代码将$iar中的每条记录分为条件,以及条件体
接着判断正则$labelRule2所表示的字符串是否包含于条件体中,默认是不包含的,并且$labelRule3中包含的{else}字符串是出现在条件体中的,所以进入循环,此时将条件体又分为两块,
分别代表两种不同的css样式,接着就是触发漏洞的核心,在这里也发现了eval函数的调用,用于代码执行的经典函数
如上图所示,将$strif变量与if条件进行了拼接,那么此处是否存在代码注入的情况?的确如此,此时可以看看$strif的值
其中第95条就包含有我们的payload,那么此时将payload和if条件进行拼接可以得到:
if(1)phpinfo();if(1)
此时成功闭合了php语句,并且跟后面的$ifFlag条件体也成功闭合了,所以能够成功进行代码执行!!!代码注入真刺激~,到此已经实现RCE,那么想想为啥会造成这样的漏洞,我向上看看变量是如何传递过来的,
parse函数就是在main.class.php这个类文件中定义的处理if代码块的函数,其入口参数为$content,那么回到调用parseIf函数的地方,也就是search.php,因为我们漏洞文件也在该文件,那么我们POST传递过来的payload最终会传递到content然后再进入到parseIf函数进行处理,而该处调用又是存在于echoPageSearch()函数中,那么回到该函数入口处,在其上方发现了对其的调用,现在在此文件全局搜索以下POST字符串
如上图所示,没有搜索到POST,那么有可能包含在common.php中,进去看看
正与我们所猜测的一样,此时可以在注释中发现,其通过一段循环将POST中的值注册为变量了,并且我们提交的标量里不能包含cfg_和GLOVALS字符串,并且在COOKIE中不能够设置,这里是为了防止变量覆盖
接下来还调用了_RunMagicQuotes()函数对变量值进行过滤,实际上进行了一个addslashes()函数的操作,并不影响我们实际所用的payload
可以从上图看到传递进来的变量直接注册为内部的变量了,并且变量内容没有发生变化,那么说明都是我们可以控制的,因为最后parse处理的变量是$content,那么我们需要弄清$order是如何赋值到$content中的,
再次文件中搜索$order的使用
可以找到4处调用,第一处是在函数内部global来引用,第二处又将$order赋值给$orderStr,但是这样赋值没有影响到$content变量,因此看最后一处调用
如上图所示,将$content中的{searchpage:ordername}部分替换为了$order变量的值,为了更清楚的看看$content的内容是如何变化的,我们可以在158行和160行处下断点,重新执行一次payload,此时可以在此断点处查看$content的值,并将替换前后的$content值进行对比,可以看到str_replace()函数将进行3处替换
即在此处完成了payload对$content变量的注入,之后在最终调用parseIf()函数处理$content变量之前,又对$content的内容进行了多次替换,但是并没有影响到我们的payload,接下来看看程序是如何解析出来我们的paylaod的
上面已经说过程序是利用
{if:(.*?)}(.*?){end if}
这一串正则来对$content变量进行匹配的,那么此时对于我们注入payload的部分,最终是eval()中包含$strIf变量,那么由于是贪婪匹配,所以首先将会匹配到第一部分{if:\"}{end if} 将“匹配出来作为一部分,然后下一次匹配再匹配到1)phpinfo();if(1作为另一次匹配的部分
可以从$iar变量的值中验证我们的推理是正确的,这里存在3处相同的匹配是因为之前$order对$content进行了3次payload注入,这样的构造的确很巧妙,首先1)phpinfo();if(1这一部分要闭合后面eval部分,然后再要满足前面正则匹配的逻辑能够把payload完整匹配出来,
这可能也是开发人员没有想到的,之后拼接的方法上面已经讲了,漏洞的整个分析流程到此结束。
5.修复方法:
在64行添加
$order = ($order == \"commend\" || $order == \"time\" || $order == \"hit\") ? $order : \"\";