环境配置
Springboot:2.7.5
依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
application.yml
spring:
mvc:
view:
prefix: /WEB-INF/jsp/
suffix: .jsp
web:
resources:
static-locations: classpath:/templates/
server:
port: 8081
前置知识
multipart/form-data
multipart/form-data这种编码方式的表单会以二进制流的方式来处理表单数据,这种编码方式会把文件域指定文件的内容也封装到请求参数里。通常会见到配合method=post去搭配使用,而后端采取inputstream等方式读取客户端传入的二进制流来处理文件。
00截断问题
PHP中:PHP\<5.3.29,且GPC关闭
Java中:
同时考虑到00截断绕过的问题,在JDK1.7.0_40(7u40)开始对\00进行了检查:
final boolean isInvalid(){
if(status == null){
status=(this.path.indexOf('\u0000')<0)?PathStatus.CHECKED:PathStatus.INVALID;
}
return status == PathStatus.INVALID;
}
在7u40后这个问题也就修复了
表单中的enctype
- application/x-www-form-urlencoded:默认编码方式,只处理表单中的value属性值,这种编码方式会将表单中的值处理成URL编码方式
- multipart/form-data:以二进制流的方式处理表单数据,会把文件内容也封装到请求参数中,不会对字符编码
- text/plain:把空格转换为+ ,当表单action属性为mailto:URL形式时比较方便,适用于直接通过表单发送邮件方式
处理文件时常用方法
separatorChar
主要用来做分隔符,防止因为跨平台时各个操作系统之间分隔符不一样出现问题
public static final char separatorChar
与系统有关的默认名称分隔符。此字段被初始化为包含系统属性 file.separator 值的第一个字符。在 UNIX 系统上,此字段的值为 '/';在 Microsoft Windows 系统上,它为 '**\'
separator
主要用来做分隔符,防止因为跨平台时各个操作系统之间分隔符不一样出现问题
public static final String separator = "" + separatorChar;
其实separator是由separatorChar转换成的,所以只是类型不同
equalsIgnoreCase
将字符串与指定的对象比较,不考虑大小写。文件上传中主要用于判断文件文件后缀名
可以与equlas对比来看,s1和s2只有大小写不同,如果用equals则返回false,equalsIgnoreCase返回true
String s1 = "SENTIMENT";
String s2 = "sentiment";
System.out.println(s1.equals(s2)); //false
System.out.println(s1.equalsIgnoreCase(s2)); //true
常见文件上传方式
文件流上传
@RequestMapping("/upload1")
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 "";
}
上传入口
<h1>文件流上传</h1>
<form method="POST" enctype="multipart/form-data" action="http://127.0.0.1:8081/upload1">
<input type="file" name="file">
<input type="submit" name="submit">
</form>
MultipartFile方式上传
MultipartFile常用方法
- String getOriginalFilename():获取上传文件的原名
- InputStream getInputStream():获取文件流
- void transferTo(File dest):将上传文件保存到一个目录文件中
- String getContentType():获取上传文件的MIME类型
@RequestMapping("/file2")
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 "";
}
若要对上传内容进行限制则可设置:
springboot
spring:
servlet:
multipart:
enabled: true
# 单文件大小
max-file-size: 100MB
# 文件达到多少磁盘写入
file-size-threshold: 4MB
springmvc
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 需要与jsp中的pageEncoding配置一致,默认为iso-8859-1-->
<property name="defaultEncoding" value="utf-8"/>
<!-- 单文件大小,单位为字节10485700=100M-->
<property name="maxUploadSize" value="10485700"/>
<!-- 文件达到多少磁盘写入-->
<property name="maxInMemorySize" value="409600"/>
</bean>
上传入口
<h1>MultipartFile上传</h1>
<form method="POST" enctype="multipart/form-data" action="http://127.0.0.1:8081/upload2">
<input type="file" name="file">
<input type="submit" name="submit">
</form>
ServletFileUpload上传
基于Commons-FileUpload组件
依赖
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
Springboot环境需关闭multipart
spring:
servlet:
multipart:
enabled: false
创建步骤
- 创建磁盘工厂:DiskFileItemFactory factory = new DiskFileItemFactory();
- 创建处理工具:ServletFileUpload upload = new ServletFileUpload(factory);
- 设置上传文件大小:upload.setFileSizeMax(3145728);
- 接收全部内容:List items = upload.parseRequest(request);
@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);
//三个照顾要上传的文件大小
fileupload.setFileSizeMax(3145728);
//判断是否为multipart/form-data类型,为false则直接跳出该方法
if (!fileupload.isMultipartContent(request)) {
return;
}
//使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
List<FileItem> items = fileupload.parseRequest(request);
for (FileItem item : items) {
//isFormField方法用于判断FileItem类对象封装的数据是否属于一个普通表单字段,还是属于一个文件表单字段,如果是普通表单字段则返回true,否则返回false。
if (item.isFormField()) {
String name = item.getFieldName();
//解决普通输入项的数据的中文乱码问题
String value = item.getString("UTF-8");
String value1 = new String(name.getBytes("iso8859-1"), "UTF-8");
System.out.println(name + " : " + value);
System.out.println(name + " : " + value1);
} else {
//获得上传文件名称
String fileName = item.getName();
System.out.println(fileName);
if (fileName == null || fileName.trim().equals("")) {
continue;
}
//注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如: c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt
//处理获取到的上传文件的文件名的路径部分,只保留文件名部分
fileName = fileName.substring(fileName.lastIndexOf(File.separator) + 1);
//获取item中的上传文件的输入流
InputStream is = item.getInputStream();
FileOutputStream fos = new FileOutputStream(filePath + File.separator + fileName);
byte buffer[] = new byte[1024];
int length = 0;
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();
}
}
}
上传入口
<h1>ServletFileUpload上传</h1>
<form method="POST" enctype="multipart/form-data" action="http://127.0.0.1:8081/upload3">
<input type="file" name="file">
<input type="submit" name="submit">
</form>
Servlet Part上传
Servlet3之后,有提出了request.getParts()
获取上传文件的方式。
除此外若加上注解@MultipartConfig,则可定义一些上传属性
方法 | 类型 | 是否可选 | 作用 |
---|---|---|---|
fileSizeThershold | int | 是 | 当前数据量大于该值时,内容将被写入文件 |
location | String | 是 | 存放文件的路径 |
maxFileSize | long | 是 | 允许上传的文件最大值,默认为-1,表示没有限制 |
maxRequestSize | long | 是 | 针对multipart/form-data 请求的最大数量,默认为-1,表示没有限制 |
ServletPart常用方法
- String getName() 获取这部分的名称,例如相关表单域的名称
- String getContentType() 如果Part是一个文件,那么将返回Part的内容类型,否则返回null(可以利用这一方法来识别是否为文件域)
- Collection getHeaderNames() 返回这个Part中所有标头的名称
- String getHeader(String headerName) 返回指定标头名称的值
- void write(String path) 将上传的文件写入服务器中项目的指定地址下,如果path是一个绝对路径,那么将写入指定的路径,如果path是一个相对路径,那么将被写入相对于location属性值的指定路径。
- InputStream getInputStream() 以inputstream的形式返回上传文件的内容
@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属性值,获取filename
Part part = request.getPart("file");
if(part == null) {
return ;
}
String filename = filePath + File.separator + part.getSubmittedFileName();
part.write(filename);
part.delete();
}
文件上传入口
<h1>ServletPart上传</h1>
<form method="POST" enctype="multipart/form-data" action="http://127.0.0.1:8081/upload4">
<input type="file" name="file">
<input type="submit" name="submit">
</form>
文件上传漏洞
上述都是no waf的文件上传方式,若不做任何防御的情况下,可以实现任意文件上传,造成文件上传漏洞
通过上述任意方法,上传jsp马
<%
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));
}
%>
执行成功
防御
content-type白名单
//1、MIME检测
String contentType = file.getContentType();
String[] white_type = {"image/gif","image/jpeg","image/jpg","image/png"};
Boolean ctFlag = false;
for (String suffix:white_type){
if (contentType.equalsIgnoreCase(suffix)){
ctFlag = true;
break;
}
}
if (!ctFlag){
return "content-type not allow";
}
如果单设置这一个的话其实很好绕过
重命名文件
可以用uuid、md5、时间戳等方式
//2、重命名文件
String uuid = UUID.randomUUID().toString();
fileName = uuid+fileName.substring(fileName.lastIndexOf("."));;
后缀白名单
//3、后缀白名单
String fileSuffix = fileName.substring(fileName.lastIndexOf("."));
String[] white_suffix = {"gif","jpg","jpeg","png"};
Boolean fsFlag = false;
for (String suffix:white_suffix){
if (contentType.equalsIgnoreCase(fileSuffix)){
fsFlag = true;
break;
}
}
if (!fsFlag){
return "suffix not allow";
}
绕过MIME检测后,可以通过白名单进行进一步的防御
修改存储位置
可以将图片存放到不可访问的路径,例如:Servlet的WEB-INF下,默认情况是访问不到的
//4、修改存储位置
String filePath = request.getServletContext().getRealPath("/WEB-INF/upload");
最终代码
public String MultiFileUpload(@RequestParam("file") MultipartFile file ,HttpServletRequest request) {
if (file.isEmpty()) {
return "请上传文件";
}
// String filePath = request.getServletContext().getRealPath("upload");
String fileName = file.getOriginalFilename();
//1、MIME检测
String contentType = file.getContentType();
String[] white_type = {"image/gif","image/jpeg","image/jpg","image/png"};
Boolean ctFlag = false;
for (String suffix:white_type){
if (contentType.equalsIgnoreCase(suffix)){
ctFlag = true;
break;
}
}
if (!ctFlag){
return "content-type not allow";
}
//2、重命名文件
String uuid = UUID.randomUUID().toString();
fileName = uuid+fileName.substring(fileName.lastIndexOf("."));;
//3、后缀白名单
String fileSuffix = fileName.substring(fileName.lastIndexOf("."));
String[] white_suffix = {"gif","jpg","jpeg","png"};
Boolean fsFlag = false;
for (String suffix:white_suffix){
if (contentType.equalsIgnoreCase(fileSuffix)){
fsFlag = true;
break;
}
}
if (!fsFlag){
return "suffix not allow";
}
//4、修改存储位置
String filePath = request.getServletContext().getRealPath("/WEB-INF/upload/");
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 "";
}
代码审计中常见文件上传关键字
DiskFileItemFactory
@MultipartConfig
MultipartFile
File
upload
InputStream
OutputStream
write
fileName
filePath