Java-Sec-Code学习1-文件上传漏洞
case1
url: http://127.0.0.1:8080/file/any
这是一个典型的上传页面,我们尝试上传一个文件试试看。
直接上传一个jsp文件,发现可以直接上传,非常顺利。这意味这后端和前端都没有对文件类型进行任何限制。
此外也提供了保存位置的回显,那么我们可以去尝试访问一下。访问不了,看来很有可能是存文件在linux 的临时目录/tmp里了而不是web目录下。
看一下后端源码怎么写的:
@PostMapping({"/upload"})
public String singleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
return "redirect:/file/status";
} else {
try {
byte[] bytes = file.getBytes();
Path path = Paths.get("/tmp/" + file.getOriginalFilename());
Files.write(path, bytes, new OpenOption[0]);
redirectAttributes.addFlashAttribute("message", "You successfully uploaded '/tmp/" + file.getOriginalFilename() + "'");
} catch (IOException var5) {
IOException e = var5;
redirectAttributes.addFlashAttribute("message", "upload failed");
this.logger.error(e.toString());
}
return "redirect:/file/status";
}
}
只要文件不空,就直接保存文件到/tmp/+文件名的目录下。这里没有对文件类型进行任何的处理、过滤和限制,就直接调用了Files.write()方法去保存文件。这里其实还有另一个问题,这个目录拼接的方式,可以尝试进行目录跨越。恶意文件可以保存到其他有写权限的目录中,直接通过…/来实习目录跨越。
最后,通过redirectAttributes.addFlashAttribute()来返回提示到前端,并重定向到/file/status路由。那么/file/status路由主要用来返回上传状态。
Case2
URL:http://127.0.0.1:8080/file/pic
这是一个图片上传的case,看起来应该是做了过滤。我们看看还能不直接上传。
果然被识别到了非法图片,那我们试试看修改content-type看看能不能绕过。
没有成功,看来是判断的后缀,png也不行成功。让我们看看他具体的后端代码:
@PostMapping({"/upload/picture"})
@ResponseBody
public String uploadPicture(@RequestParam("file") MultipartFile multifile) throws Exception {
if (multifile.isEmpty()) {
return "Please select a file to upload";
} else {
String fileName = multifile.getOriginalFilename();
String Suffix = fileName.substring(fileName.lastIndexOf("."));
String mimeType = multifile.getContentType();
String filePath = "/tmp/" + fileName;
File excelFile = this.convert(multifile);
String[] picSuffixList = new String[]{".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"};
boolean suffixFlag = false;
String[] mimeTypeBlackList = picSuffixList;
int var10 = picSuffixList.length;
int var11;
for(var11 = 0; var11 < var10; ++var11) {
String white_suffix = mimeTypeBlackList[var11];
if (Suffix.toLowerCase().equals(white_suffix)) {
suffixFlag = true;
break;
}
}
if (!suffixFlag) {
this.logger.error("[-] Suffix error: " + Suffix);
this.deleteFile(filePath);
return "Upload failed. Illeagl picture.";
} else {
mimeTypeBlackList = new String[]{"text/html", "text/javascript", "application/javascript", "application/ecmascript", "text/xml", "application/xml"};
String[] var15 = mimeTypeBlackList;
var11 = mimeTypeBlackList.length;
for(int var18 = 0; var18 < var11; ++var18) {
String blackMimeType = var15[var18];
if (SecurityUtil.replaceSpecialStr(mimeType).toLowerCase().contains(blackMimeType)) {
this.logger.error("[-] Mime type error: " + mimeType);
this.deleteFile(filePath);
return "Upload failed. Illeagl picture.";
}
}
boolean isImageFlag = isImage(excelFile);
this.deleteFile(randomFilePath);
if (!isImageFlag) {
this.logger.error("[-] File is not Image");
this.deleteFile(filePath);
return "Upload failed. Illeagl picture.";
} else {
try {
byte[] bytes = multifile.getBytes();
Path path = Paths.get("/tmp/" + multifile.getOriginalFilename());
Files.write(path, bytes, new OpenOption[0]);
} catch (IOException var14) {
this.logger.error(var14.toString());
this.deleteFile(filePath);
return "Upload failed";
}
this.logger.info("[+] Safe file. Suffix: {}, MIME: {}", Suffix, mimeType);
this.logger.info("[+] Successfully uploaded {}", filePath);
return String.format("You successfully uploaded '%s'", filePath);
}
}
}
}
从源代码中可以看到后端会获取后缀和content-type。
后缀采用白名单如图中的图片后缀,后缀获取时通过找最后一个.符号后的子串。
后续对content-type的对比,使用了黑名单策略,出现数组中的这些MIME类型都不行。
那为什么刚才我们给webshell改后缀和content-type还是不行呢?破案了,原来是这里还有一个是否图片的检查,我们跟进isImage方法看看。
看起来是利用了java.awt.image.ImageIO#read()方法来解析图片,我们的是webshell肯定无法成功解析,因此bi实际上为null了,返回值自然为false。也就出现了文件不是图片的提示了。
isImage()检查通过后,文件将会被保存。
正常的这个流程,已经是无法实现写非图片文件了。但是你可以注意到在一开始上传时实际上会调用一个自定义的convert函数来保存临时文件,实际上他还是写了临时文件到预设的目录。只不过是通过UUID来命名,没有提供相关回显出来,无法知道其名称。
但是,无法否认的是恶意文件确实被写入了系统。
你可能会发现,实际上代码中有一个deleteFile的方法用来删除文件,主要用在文件不合规的时候删除文件。但是这里有一个问题在于,发现文件不合规后删除的文件居然是filePath变量的路径,这里我不理解啊。没有成功通过所有检查根本就不会吧filePath作为文件路径去保存文件,这里的删除毫无意义啊。我理解是不是想要把convert出来的临时文件给删除了,但是这里写错了。如果需要删除临时文件的话,那这个convert方法就必须多加一个返回值了,把这个randomPath给回传出来,在deleteFile里传入去删除。只有这样,才能确保及时恶意文件临时落地后,检查一旦不通过就会被清除。
以上是我个人的一些理解,我觉得这个deleteFile有点不合理。