漏洞概述
非法的文件上传是黑客最常用的攻击手段之一,如果允许黑客向某个可通过 Web 访问的目录上传文件,并能够将这些文件传递给代码解释器,它会给正常的系统中执行恶意的代码文件(通常这种恶意代码叫做:WebShell),通过执行恶意的WebShell获取相应权限,来控制整个主机系统。
安全措施
限制文件扩展名
检查上传的文件扩展名是否为预期的扩展名文件,拒绝接收任何非预期的扩展名文件。
检查文件头格式
仅作文件扩展名检查,不足以识别一个文件的真正文件格式,可以进一步检查文件头,以识别上传文件的格式。不接收非预期文件头格式的文件。
常见文件的文件头(16进制,摘自百度百科,仅供参考)
JPEG (jpg),文件头:FFD8FF
PNG (png),文件头:89504E47
GIF (gif),文件头:47494638
TIFF (tif),文件头:49492A00
Windows Bitmap (bmp),文件头:424D
CAD(dwg),文件头:41433130
Adobe Photoshop (psd),文件头:38425053
Rich Text Format (rtf),文件头:7B5C727466
XML (xml),文件头:3C3F786D6C
HTML(html),文件头:68746D6C3E
Email [thorough only] (eml),文件头:44656C69766572792D646174653A
Outlook Express(dbx),文件头:CFAD12FEC5FD746F
Outlook (pst),文件头:2142444E
MS Word/Excel(xls.or.doc),文件头:D0CF11E0
MS Access (mdb),文件头:5374616E64617264204A
WordPerfect (wpd),文件头:FF575043
Adobe Acrobat (pdf),文件头:255044462D312E
Quicken (qdf),文件头:AC9EBD8F
Windows Password (pwl),文件头:E3828596
ZIP Archive (zip),文件头:504B0304
RAR Archive (rar),文件头:52617221
Wave(wav),文件头:57415645
AVI (avi),文件头:41564920
Real Audio(ram),文件头:2E7261FD
Real Media (rm),文件头:2E524D46
MPEG(mpg),文件头:000001BA
MPEG (mpg),文件头:000001B3
Quicktime(mov),文件头:6D6F6F76
Windows Media (asf),文件头:3026B2758E66CF11
MIDI(mid),文件头:4D546864
注意:文件头是可篡改的。
文件内容检查
对于文件内容格式较为固定的情况,可就文件内容做检查,进一步确保上传文件的安全性,如上传文件内容格式不满足预期,则不予接收。
随机命名文件
将接收的文件随机重命名(此举对于需要回显文件的情况没有意义)。
非WEB目录存储
对于不需要回显文件的情况,可将接收的文件保存到非WEB发布目录下,这样可以保证,即使攻击者上传了WebShell,也不可以访问执行。
代码参考
参考代码功能:
1、通过上传文件的后缀名判断上传的文件类型是否合法
2、如果合法则在根据上传文件头信息进一步判断上传文件的合法性
3、最后随机重命名上传的文件名以保证上传文件的安全性
package com.unisguard.fileupload.controller;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import com.unisguard.fileupload.service.FileCheckService;
@Controller
public class FileUploadController {
@Autowired
private FileCheckService fileCheckService;
@RequestMapping("/upload")
public String handleFormUpload(HttpServletRequest request,MultipartHttpServletRequest multipartRequest,
HttpServletResponse response,Model model) throws Exception {
try {
StringBuilder messages = new StringBuilder();
Iterator<String> i = multipartRequest.getFileNames();
while(i.hasNext()) {
MultipartFile file = multipartRequest.getFile(i.next());
if (!file.isEmpty()) {
InputStream source = file.getInputStream();
// 得到上传的文件的文件名
String fileFullName = file.getOriginalFilename();
// 检测文件大小
boolean isValidFileSize = fileCheckService.checkFileSize(file);
if(!isValidFileSize) {
messages.append("文件[").append(fileFullName).append("]上传失败,原因:文件大小超过了10M!");
continue;
}
// 通过文件名检测文件后缀
boolean isValidFileExt = fileCheckService.checkFileExt(fileFullName);
if(!isValidFileExt) {
messages.append("文件[").append(fileFullName).append("]上传失败,原因:文件格式不允许!");
continue;
}
// 通过文件头检测文件后缀
isValidFileExt = fileCheckService.checkFileHeaderType(file);
if(!isValidFileExt) {
messages.append("文件[").append(fileFullName).append("]上传失败,原因:文件格式不允许,格式可能被篡改!");
continue;
}
// 获取保存服务器的文件名
String newFileName = fileCheckService.createNewFile(fileFullName);
// 得到上传服务器的路径
String pathname = new StringBuilder("D:/upload/").append(newFileName).toString();
File destination = new File(pathname);
// 文件流写到服务器端
FileUtils.copyInputStreamToFile(source, destination);
}
}
if(messages.length() != 0) {
model.addAttribute("message", messages.toString());
} else{
model.addAttribute("message", "上传成功!");
}
} catch (IOException e) {
e.printStackTrace();
throw new Exception("上传出错!",e);
}
return "message";
}
}