任意文件上传(高危)
# 漏洞等级
高危
# 漏洞描述
现在大多数网站都有文件上传功能,用于上传文档、图片等操作。但是未对上传的文件进行严格的验证和过滤,就有可能导致攻击者上传恶意脚本文件,获取信息、控制网站,甚至控制服务器
# 漏洞危害
上传恶意脚本,获取信息、控制网站,甚至控制服务器
# 修复建议 (注意全局修复)
1、在服务器上使用白名单对上传的文件进行严格的验证,只允许上传特定的文件类型。验证包括文件扩展名校验、文件类型(Content-Type)校验、MIME类型校验等等。
2、上传文件的存储目录禁用执行权限
# 修复参考代码
```java
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.uwit.bsdigistersystem.modules.recordsconfig.constant.DictUtils;
import com.uwit.bsdigistersystem.modules.recordsconfig.entity.Dictdata;
import com.uwit.common.utils.ResultUtil;
import com.uwit.common.utils.file.Extension;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.io.IOUtils;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Description 文件上传拦截器
* @Author long
* @Version V1.0.0
* @Since 1.0
* @Date 2022/5/27
*/
@Slf4j
public class FileHeaderCheckInterceptorImpl implements HandlerInterceptor {
/**
* 目标方法执行以前
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver(request.getSession().getServletContext());
// 判断是否为文件上传请求
if (commonsMultipartResolver.isMultipart(request)) {
MultipartResolver resovler = new StandardServletMultipartResolver();
MultipartHttpServletRequest multipartRequest = resovler.resolveMultipart(request);
Map<String, MultipartFile> files = multipartRequest.getFileMap();
for (String formKey : files.keySet()) {
MultipartFile multipartFile = multipartRequest.getFile(formKey);
String fileExt = Extension.getFileExtensionByMultipartFile(multipartFile);
// 后缀判断
boolean fixPass = this.checkFileSuffix(multipartFile);
if (!fixPass) {
this.responseError(response);
log.error(fileExt + "此文件后缀系统不允许上传");
return false;
}
// 文件头判断
if (!this.checkFileHeader(multipartFile)) {
this.responseError(response);
String fileHead = getFileHeader(multipartFile);
log.error(fileHead + "此文件头不在系统允许列表内");
return false;
}
}
}
return true;
}
/**
* 获取前8位文件头
* @param multipartFile
* @return 文件头
*/
public static String getFileHeader(MultipartFile multipartFile) {
byte[] file = new byte[0];
try {
file = multipartFile.getBytes();
} catch (IOException e) {
e.printStackTrace();
}
int HEADER_LENGTH = 4;
if (file.length > HEADER_LENGTH) {
//转成16进制
StringBuilder sb = new StringBuilder();
for (int i = 0; i < HEADER_LENGTH; i++) {
int v = file[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
sb.append(0);
}
sb.append(hv);
}
return sb.toString();
}
return "";
}
/**
* 检测文件头
*
* @param multipartFile 上传的文件对象
* @return true: 符合,false: 不符合
*/
public Boolean checkFileHeader(MultipartFile multipartFile) {
// 验证文件头
String fileHead = getFileHeader(multipartFile);
fileHead = fileHead.toLowerCase();
Set<String> systemAllowFileHeardSet = getSystemAllowFileHeardSet();
return systemAllowFileHeardSet.contains(fileHead);
}
/**
* 获取系统允许上传的问题文件类型后缀列表
* @return 系统允许上传的文件后缀列表
*/
public Set<String> getSystemAllowFileTypeExtSet(){
List<Dictdata> sysFormatExtList = DictUtils.getDictCache("documentFormat");
if(CollectionUtil.isEmpty(sysFormatExtList)){
sysFormatExtList = new ArrayList<>();
}
return sysFormatExtList.stream()
.map(Dictdata::getText)
.map(String::toLowerCase)
.collect(Collectors.toSet());
}
/**
* 获取系统允许上传的问题文文件头列表
* @return 系统允许上传的文件头列表
*/
public Set<String> getSystemAllowFileHeardSet(){
List<Dictdata> sysFormatExtList = DictUtils.getDictCache("documentFormat");
if(CollectionUtil.isEmpty(sysFormatExtList)){
sysFormatExtList = new ArrayList<>();
}
return sysFormatExtList.stream()
.map(Dictdata::getValue)
.map(String::toLowerCase)
.collect(Collectors.toSet());
}
/**
* 文件后缀判断
*
* @param multipartFile 文件对象
* @return true :符合,false:不符合
*/
public Boolean checkFileSuffix(MultipartFile multipartFile) {
assert multipartFile != null;
String fileExt = Extension.getFileExtensionByMultipartFile(multipartFile);
fileExt = fileExt.toLowerCase();
Set<String> systemExtSet = getSystemAllowFileTypeExtSet();
return systemExtSet.contains(fileExt);
}
public void responseError(HttpServletResponse response) {
log.error("----------非系统可信文档类型,不支持采集上传!");
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control", "no-cache");
try {
response.getWriter().println(JSON.toJSONString(ResultUtil.error("500", "非系统可信文档类型,不支持采集上传!")));
response.getWriter().flush();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 目标方法执行以后
*
* @param request 请求
* @param response 响应
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 渲染以后
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
```