java-sec-code中的文件上传
这里仅讨论文件上传,不讨论后续利用(中间件解析漏洞等不做讨论)
任意文件伤害上传
any-->uploada.html-->upload
访问any路由时,会出现upload.html的上传文件界面,指向upload路由
@GetMapping("/any")
public String index() {
return "upload"; // return upload.html page
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h3>file upload</h3>
<form method="POST" th:action="upload" enctype="multipart/form-data">
<input type="file" name="file" /><br/><br/>
<input type="submit" value="Submit" />
</form>
</body>
</html>
private static final String UPLOADED_FOLDER = "D:\\alibaba\\upload\\";
@PostMapping("/upload")
public String singleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
if (file.isEmpty()) {//接受请求数据包中的file参数,如果为null,则输出Please select a file to upload
redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
return "redirect:/file/status";
}
try {
// Get the file and save it somewhere
byte[] bytes = file.getBytes();//读取上传的文件内容
Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());//这里的UPLOADED_FOLDER是开头定义的保存文件路径,与上传的文件名拼接。
Files.write(path, bytes);//在拼接完成的文件路径内写入数据
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded '" + UPLOADED_FOLDER + file.getOriginalFilename() + "'");//输出上传成功信息
} catch (IOException e) {
redirectAttributes.addFlashAttribute("message", "upload failed");
logger.error(e.toString());
}
return "redirect:/file/status";
}
代码段并未对用户上传的文件进行任何过滤,导致任意文件上传漏洞
只允许上传图片
pic-->uploadPic.html-->/upload/picture
@GetMapping("/pic")
public String uploadPic() {
return "uploadPic";
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h3>file upload only picture</h3>
<form method="POST" th:action="@{upload/picture}" enctype="multipart/form-data">
<input type="file" name="file" /><br/><br/>
<input type="submit" value="Submit" />
</form>
</body>
</html>
@PostMapping("/upload/picture")
@ResponseBody
public String uploadPicture(@RequestParam("file") MultipartFile multifile) throws Exception {//从请求参数中获取file的属性值,并将其赋值给multifile
if (multifile.isEmpty()) {
return "Please select a file to upload";
}//检测是否为空
String fileName = multifile.getOriginalFilename();//获取文件名
String Suffix = fileName.substring(fileName.lastIndexOf(".")); // 获取文件后缀名
String mimeType = multifile.getContentType(); // 获取MIME类型
String filePath = UPLOADED_FOLDER + fileName;//拼接上传路径和文件名
File excelFile = convert(multifile);//将multifile传入convert方法中 在最下面
// 判断文件后缀名是否在白名单内 校验1
String[] picSuffixList = {".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"};
boolean suffixFlag = false;
for (String white_suffix : picSuffixList) {
if (Suffix.toLowerCase().equals(white_suffix)) {
suffixFlag = true;
break;
}
}//将后缀名转换为小写与白名单中的数据进行比对
if (!suffixFlag) {//如果不在白名单中,则进入方法中
logger.error("[-] Suffix error: " + Suffix);
deleteFile(filePath);
return "Upload failed. Illeagl picture.";
}
// 判断MIME类型是否在黑名单内 校验2
String[] mimeTypeBlackList = {
"text/html",
"text/javascript",
"application/javascript",
"application/ecmascript",
"text/xml",
"application/xml"
};
for (String blackMimeType : mimeTypeBlackList) {
// 用contains是为了防止text/html;charset=UTF-8绕过
if (SecurityUtil.replaceSpecialStr(mimeType).toLowerCase().contains(blackMimeType)) {//replaceSpecialStr方法在最下面,去除MIME类型中除数字小写字母.点/斜杠-破折号之外的其他字符
logger.error("[-] Mime type error: " + mimeType);
deleteFile(filePath);
return "Upload failed. Illeagl picture.";
}
}
// 判断文件内容是否是图片 校验3
boolean isImageFlag = isImage(excelFile);//使用ImageIO类的read方法,检测是否为JPEG、PNG、GIF 等图片格式文件
deleteFile(randomFilePath);
if (!isImageFlag) {
logger.error("[-] File is not Image");
deleteFile(filePath);
return "Upload failed. Illeagl picture.";
}
try {
// Get the file and save it somewhere
byte[] bytes = multifile.getBytes();//获取文件内容
Path path = Paths.get(UPLOADED_FOLDER + multifile.getOriginalFilename());//拼接完整路径
Files.write(path, bytes);//写入文件内容
} catch (IOException e) {
logger.error(e.toString());
deleteFile(filePath);
return "Upload failed";
}
logger.info("[+] Safe file. Suffix: {}, MIME: {}", Suffix, mimeType);
logger.info("[+] Successfully uploaded {}", filePath);
return String.format("You successfully uploaded '%s'", filePath);
}
private void deleteFile(String filePath) {
File delFile = new File(filePath);//新建一个file类
if(delFile.isFile() && delFile.exists()) {//检测对应目录下是否有相应文件
if (delFile.delete()) {//进行删除
logger.info("[+] " + filePath + " delete successfully!");
return;
}
}
logger.info(filePath + " delete failed!");
}
/**
* 为了使用ImageIO.read()
*
* 不建议使用transferTo,因为原始的MultipartFile会被覆盖
* https://stackoverflow.com/questions/24339990/how-to-convert-a-multipart-file-to-file
*/
private File convert(MultipartFile multiFile) throws Exception {
String fileName = multiFile.getOriginalFilename();//获取文件名
String suffix = fileName.substring(fileName.lastIndexOf("."));//获取后缀名
UUID uuid = Generators.timeBasedGenerator().generate();//生成一个唯一的字符串标识uuid
randomFilePath = UPLOADED_FOLDER + uuid + suffix;
// 拼接路径,uuid和后缀名
File convFile = new File(randomFilePath);//创建一个新的对象,路径由randomFilePath指定
boolean ret = convFile.createNewFile();//创建一个新的文件
if (!ret) {
return null;
}//检测创建文件是否成功
FileOutputStream fos = new FileOutputStream(convFile);//创建一个新的FileOutputStream用于向convfile中写入内容
fos.write(multiFile.getBytes());//写入文件内容
fos.close();
return convFile;
}
/**
* Check if the file is a picture.
*/
private static boolean isImage(File file) throws IOException {
BufferedImage bi = ImageIO.read(file);
return bi != null;
}
}
public static String replaceSpecialStr(String str) {
StringBuilder sb = new StringBuilder();
str = str.toLowerCase();
for(int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
// 如果是0-9
if (ch >= 48 && ch <= 57 ){
sb.append(ch);
}
// 如果是a-z
else if(ch >= 97 && ch <= 122) {
sb.append(ch);
}
else if(ch == '/' || ch == '.' || ch == '-'){
sb.append(ch);
}
}
return sb.toString();
}
表面上使用了多种方式验证了用户上传的文件,但实际上存在诸多问题
一,如果用户上传的是其他类型文件(例如jsp文件),虽然过不了后缀名白名单和ImageIO.read
方法过滤,但是/upload/picture
方法开头有个convert
方法,在未经任何过滤的前提下,进行了文件写入,虽然有个uuid使得上传后的文件名随机,但实际上的文件内容的确被写入到随机命名的jsp文件中,演示
创建一个名为test.jsp
的文件,内容随机,进行上传,对应目录下生成的新文件内容和test.jsp
一模一样,如果存在目录遍历漏洞则可以进行组合利用,
修改方案:在代码107行修改为 deleteFile(randomFilePath);
在后缀名不符合时,删除临时文件即可,
二,deleteFile
方法的错误利用
首先在convert
方法中,上传的文件被重新命名,但deleteFile
方法获取的filePath
参数是方法开头直接获取的用户上传的文件名,然后直接进行删除
,在
String[] picSuffixList = {".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"};
boolean suffixFlag = false;
for (String white_suffix : picSuffixList) {
if (Suffix.toLowerCase().equals(white_suffix)) {
suffixFlag = true;
break;
}
}
if (!suffixFlag) {
logger.error("[-] Suffix error: " + Suffix);
deleteFile(filePath);
return "Upload failed. Illeagl picture.";
}
这段代码中,如果用户上传的文件后缀名不符合这个要求,则进入deleteFile
方法,删除filePath
文件,假设上传文件夹中有个名为test.jsp
的文件,当用户上传的文件也命名为test.jsp
时,程序就会删除上传目录中的
test.jsp
文件
演示
至于是否存在任意文件删除的问题,这里不做讨论
由于ImageIO.read
必须要读取到文件,所以只能够先进行上传操作,等待检验完成,在进行删除操作