1 文件上传漏洞审计
1.1 漏洞原理介绍
大部分文件上传漏洞的产生是因为Web应用程序未对文件的格式和进行严格过滤,导致用户可上传jsp、php等webshell代码文件,从而被利用。例如在 BBS发布图片 , 在个人网站发布ZIP压缩包, 在办公平台发布DOC文件等 , 只要 Web应用程序允许上传文件, 就有可能存在文件上传漏洞。
1.2 审计策略
-
文件上传可以搜索以下关键词: upload,write,fileName ,filePath
-
在查看时,主要判断是否有检查后缀和文件的大小
-
同时要查看配置文件是否有设置白名单或者黑名单(不推荐黑名单,可能被绕过)
1.3 修复方案
-
通过后端增加对上传文件后缀格式的验证,验证的手段有两种,分别是白名单校验和黑名单校验
-
推荐使用白名单校验,绕过的危险小,黑名单无法维护全面,极容导致绕过校验成功上传脚本文件
-
只允许文件上传到固定目录且设置程序对该目录下只有读写权限,不得具有执行权限
-
推荐存储文件的服务器和应用服务器相互独立
-
上传成功的文件不得真实的存储路径回显给前端,但是对于我想下载禁止直接使用完整的路径【为了防止直接暴露真实路径到前端】,应该通过key,到服务端或数据库中找到真实的路径下载即可
-
必要的时候需要重命名文件名,最合理的一定是后端生成的文件的目录
1.4 审计案例
1.4.1 无任何过滤
服务端脚本语言未对上传的文件进行任何限制和过滤,导致恶意用户上传任意文件。
<form action="/file/upload01" method="post" enctype="multipart/form-data"> <input type="file" name="uploadfile" > <input type="submit"> </form>
上传文件的后端处理,基于Spring Boot
package com.ms08067.fileupload.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; @Controller @RequestMapping("/file") public class FileUploadController { /** * 未对上传文件进行任何安全处理 * @param file * @return */ @PostMapping("/upload01") public String uploadFile01(@RequestParam("uploadfile") MultipartFile file) { //获取文件名 String filename = file.getOriginalFilename(); //文件保存路径 // String path = "/"; File outfile = new File("D:\\20230222"+filename); try { file.transferTo(outfile); } catch (IOException e) { e.printStackTrace(); } return "success"; } }
上述代码未对上传的文件进行任何检测,可以上传任意类型的文件,包括exe文件和木马等。
运行页面如下:
成功上传后
1.4.2 客户端检测
JS对文件后缀名检测绕过
通过前端js检测文件名是否合法没有任何意义,因为任意用户都可以对前端js进行任意修改或者通过burp suite抓包修改上传的文件名。
以下代码限制上传文件的后缀名必须为.jpg和.png
<form action="/file/upload02" method="post" οnsubmit="return judge()" enctype="multipart/form-data"> <input type="file" name="uploadfile" id="checkfile" > <input type="submit" value="提交"> <p id="msg"></p> </form> <script type="text/javascript"> function judge(){ var file=document.getElementById("checkfile").value; if (file==null||file==""){ alert("请选择要上传的文件"); // location.reload(true); return false; } var isnext=false; var filetypes=[".jpg",".png"]; var fileend=file.substring(file.lastIndexOf(".")); for (var i=0;i<filetypes.length;i++){ if (filetypes[i]==fileend){ isnext=true; break; } } if (!isnext){ document.getElementById("msg").innerHTML="文件类型不允许"; // location.reload(true); return false; }else { return true; } } </script>
后端没有进行任何限制,将文件存储在src\main\resources\static\upload下
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; /** * 前端通过JS进行文件名称限制 * @param file * @return */ @PostMapping("/upload02") public String uploadFile02(@RequestParam("uploadfile") MultipartFile file) { String filename = file.getOriginalFilename(); String path = "D:\\20230222"; File fileDir = new File(path); File outfile = new File(fileDir.getAbsolutePath()+File.separator + filename); try { file.transferTo(outfile); }catch (IOException e){ e.printStackTrace(); } return "success"; }
绕过js检测方法一:修改前端代码,删除js检测部分,或者禁用js
绕过js检测方法二:使用代理Burp Suite上传文件;上传符合要求的文件类型,抓包修改文件类型。例
如要上传1.jsp,先将文件名改为1.jpg,上传,抓包,再修改为1.jsp即可。
成功上传
1.4.3 服务端检测绕过
1.4.3.1 服务器端后缀名检测绕过
主要通过黑白名单进行过滤,如果不符合过滤规则,则不允许上传
一般有个专门的 blacklist 文件,里面会包含常见的危险脚本文件后缀名。
1.4.3.2 大小写绕过
以下后端代码,检测上传的文件的后缀名是否符合要求,不允许上
传".jsp",".php",".exe",".dll","vxd","html"结尾的文件,可通过将文件后缀大写进行绕过
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; //基于黑名单大小写绕过的文件上传 @PostMapping("/upload03") public String uploadFile03(@RequestParam("uploadfile")MultipartFile file, Model model){ boolean flag=true; String filename = file.getOriginalFilename(); System.out.println(filename); String suffix=filename.substring(filename.lastIndexOf(".")); String[] blacklist={".jsp",".php",".exe",".dll","vxd","html"};//后缀名黑名单 for (String s : blacklist) { if (suffix.equals(s)){ flag=false; break; } } if (flag){ String path="D:\\20230222"; File fileDir = new File(path); File outfile = new File(fileDir.getAbsolutePath()+File.separator + filename); try { file.transferTo(outfile); return "success"; }catch (IOException e){ e.printStackTrace(); } } else { model.addAttribute("msg","非法文件类型"); } return "index"; }
通过大小写上传
上述黑名单仅过滤了少数后缀名,可以上传其他后缀类型的恶意文件。
可利用Burp suite截断HTTP请求,利用Intruder模块进行枚举后缀名,寻找黑名单中没有过滤的后缀名。接收HTTP请求,send to intruder,选中变量,在Payloads中加载相应的字典。
1.4.3.3 双写绕过
以下代码判断文件后缀名是否存在黑名单中的字符,若存在则将对应的字符串替换为空
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; //基于双写绕过的文件上传(如果后缀名为非法的字符,将替换成空)修复大小写绕过 @PostMapping("/upload04") public String uploadFile04(@RequestParam("uploadfile")MultipartFile file){ String filename = file.getOriginalFilename(); System.out.println(filename); String preFilename=filename.substring(0,filename.lastIndexOf(".")); String suffix=filename.substring(filename.lastIndexOf(".")).toLowerCase(); String[] blacklist={"jsp","php","exe","dll","vxd","html"};//后缀名黑名单 for (String s : blacklist) { if (suffix.indexOf(s)!=-1){ suffix=suffix.replace(s,"");//后缀存在黑名单字符串,则将字符串替换为空 } } String path="D:\\20230222"; File fileDir = new File(path); File outfile = new File(fileDir.getAbsolutePath()+File.separator + preFilename+suffix); try { file.transferTo(outfile); return "success"; }catch (IOException e){ e.printStackTrace(); } return "index"; }
可通过双写后缀名进行绕过,例如上传4.jjspsp文件,过滤掉jsp后,文件名正好是我们想要上传的
1.4.3.4 双后缀名绕过
以下代码,后端判断后缀名使用的是filename.indexOf("."),而不是filename.lastIndexOf("."),
可通过双后缀名绕过检测,例如欲上传1.jsp,可将文件名改为1.jsp.jsp,这样后端获得的后缀名
为.jsp.jsp,可通过检测。
//文件后缀名双写绕过-未修复大小写绕过 @PostMapping("/upload05") public String uploadFile05(@RequestParam("uploadfile")MultipartFile file,Model model){ boolean flag=true; String filename = file.getOriginalFilename(); System.out.println(filename); String suffix=filename.substring(filename.indexOf(".")); String[] blacklist={".jsp",".php",".exe",".dll",".vxd",".html"};//后缀名黑名单 for (String s : blacklist) { if (suffix.equals(s)){ flag=false; break; } } if (flag){ String path="D:\\20230222"; File fileDir = new File(path); File outfile = new File(fileDir.getAbsolutePath()+File.separator + filename); try { file.transferTo(outfile); return "success"; }catch (IOException e){ e.printStackTrace(); } }else { model.addAttribute("msg","非法文件类型"); } return "index"; }
上传不符合windows文件命名规则的文件名
上述代码可以通过抓包,修改文件名为如下形式:
-
点绕过1.jsp.
-
空格绕过1.jsp(空格)
-
1.jsp:1.jpg
-
1.jsp::$DATA
1.4.4 白名单检测
在jdk低版本(1.7及以下)中可以使用%00截断。图片木马
1.4.4.1 MIME类型检测绕过
以下代码限制上传文件的MIME类型需为"image/jpeg","image/png"或"image/gif",可通过抓包,
修改Content-Type为合法类型绕过MIME类型检测
@PostMapping("/upload07") public String uploadFile07(@RequestParam("uploadfile")MultipartFile file,Model model){ boolean flag=false; String filename = file.getOriginalFilename(); String contentType = file.getContentType(); System.out.println(filename); String preFilename=filename.substring(0,filename.lastIndexOf(".")); String suffix=filename.substring(filename.lastIndexOf(".")).toLowerCase(); //基于文件头类型进行白名单校验 String[] whiteList={"image/jpeg","image/png","image/gif"}; for (String s : whiteList) { if (contentType.equals(s)){ flag=true; } } if (flag){ String path="D:\\20230222"; File fileDir = new File(path); File outfile = new File(fileDir.getAbsolutePath()+File.separator + filename); try { file.transferTo(outfile); return "success"; }catch (IOException e){ e.printStackTrace(); } }else { model.addAttribute("msg","非法文件类型"); } return "index"; }
例如上传1.jsp文件,可修改Content-Type值为 image/jpeg
1.4.4.2 文件头检测绕过
根据文件的前面几个字节,即常说的魔术数字进行判断,不同文件类型的开头几个字节不同。
常见文件头:
以下代码,通过检测文件头部分判断上传的文件是否为图片,可利用如下两种方法绕过
package com.ms08067.fileupload.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Map; @Controller @RequestMapping("/file1") public class FileUploadController1 { public final static Map<String,String> FileType=new HashMap<String,String>(); static { getAllFileType();//初始化文件类型信息 } @PostMapping("/upload") public static String upload(@RequestParam("uploadfile") MultipartFile file, Model model){ String filename = file.getOriginalFilename(); boolean flag=false; byte[] b=new byte[50]; try { InputStream inputStream = file.getInputStream(); inputStream.read(b); System.out.println(b.toString()); StringBuilder stringBuilder=new StringBuilder(); if (b==null ||b.length<0){ flag=false; } for (int i = 0; i < b.length; i++) { int v=b[i]&0xff; String hv=Integer.toHexString(v);//十六进制 stringBuilder.append(hv); } System.out.println("stringBuilder"+stringBuilder.toString()); String fileTypeHex = String.valueOf(stringBuilder.toString()); Iterator<Map.Entry<String, String>> iterator = FileType.entrySet().iterator(); while (iterator.hasNext()){//判断文件前几个字节是否为FileType中三种类型之一 Map.Entry<String, String> next = iterator.next(); System.out.println(fileTypeHex.toUpperCase(Locale.ROOT)); if (fileTypeHex.toUpperCase(Locale.ROOT).startsWith(next.getValue())){ flag=true; } } inputStream.close(); }catch (FileNotFoundException e){ e.printStackTrace(); }catch (IOException e){ e.printStackTrace(); } if (flag){ String path="D:\\20230222"; File fileDir = new File(path); File outfile = new File(fileDir.getAbsolutePath()+File.separator + filename); try { file.transferTo(outfile); return "success"; }catch (IOException e){ e.printStackTrace(); } }else { model.addAttribute("msg","非法文件类型"); } return "index"; } private static void getAllFileType(){ FileType.put("jpeg","FFD8FF"); FileType.put("png","89504E47"); FileType.put("gif","47494638"); FileType.put("jpg","GIF89a"); } }
1.4.4.3 添加合法文件头绕过
通过抓包,添加合法文件头,例如GIF89a(jpg格式文件头)
上传两个注意事项:
(1)编码前空格、编码上边空一行
1.4.4.4 制作图片木马绕过
copy 2.jpg/b+1.jsp 3.jpg,或者使用记事本等软件打开图片,在末尾添加jsp木马数据,将攻击脚本隐藏到图片中。
单纯的图片马并不能直接和蚁剑连接,因为该文件依然是以image格式进行解析,需要结合文件包含漏洞
1.4.4.5 ImageIO判断上传图片文件
通过ImageReader解码file并返回一个BufferedImage对象,如果找不到合适的ImageReader则会返回null,我们可以认为这不是图片文件。
如果能够正常的获取到一张图片的宽高属性,那么该文件一定是图片,因为非图片文件获取不到它的宽高属性的。
但若是在可以正常打开的图片里面加入非法代码或者病毒,那就非常危险了
@Controller public class UploadImg { //ImageIO判断上传的文件是否为图片 @PostMapping("/upload") public static String uploadImg(@RequestParam("uploadfile")MultipartFile file , Model model){ boolean flag=false; String filename = file.getOriginalFilename(); String suffix = filename.substring(filename.lastIndexOf(".")); String path="src\\main\\resources\\static\\upload"; File fileDir = new File(path); File outfile = new File(fileDir.getAbsolutePath()+File.separator + filename); String[] whiteList={".jpg",".png"}; for (String s : whiteList) { if (suffix.toLowerCase(Locale.ROOT).equals(s)){ flag=true; break; } } File tmpFile=null; if (flag){ tmpFile = new File(System.getProperty("java.io.tmpdir"), filename); try{ file.transferTo(tmpFile); BufferedImage read = ImageIO.read(tmpFile); read.getWidth(); read.getHeight(); }catch (Exception e){ e.printStackTrace(); flag=false; }finally { if (flag){ try { FileCopyUtils.copy(new FileInputStream(tmpFile), Files.newOutputStream(Paths.get(path,filename), StandardOpenOption.CREATE_NEW)); tmpFile.delete(); return "success"; }catch (FileNotFoundException e){ e.printStackTrace(); }catch (IOException e){ e.printStackTrace(); } }else { model.addAttribute("msg","请上传图片文件!"); } } }else { model.addAttribute("msg","文件后缀名不符合要求"); } return "index"; } }
1.5 文件上传漏洞总结
文件上传漏洞的原因:
-
未对文件做任何过滤,可上传任意文件类型,如木马、可执行文件等;
-
仅在js端检验文件后缀,可通过删除js或禁用js或抓包修改文件后缀等方法绕过;
-
后端后缀过滤使用黑名单,过滤不全,可通过使用未过滤的后缀名、大小写变换、双写后缀名、双后缀名、文件名结尾加”.“或空格、%00截断等方式绕过;
-
后端判断文件类型,只判断Content-type,可通过抓包修改Content-type字段的值进行绕过;
-
后端检查文件内容仅检查文件头内容,可通过抓包添加合法文件头,或使用其他工具添加合法文件头进行绕过。
1.6 文件上传漏洞修复
-
服务器端的检查最好使用白名单过滤的方法,黑名单极不可靠;
-
使用随机数**改写文件名和文件路径**。文件上传如果要执行代码,需要用户能够访问到这个文件。应用了随机数改写了文件名和路径,可防止大小写绕过、双后缀、多后缀等手段,将极大地增加攻击的成本;
-
文件上传目录设置为不可执行,只要web容器无法解析该目录下面的文件,即使攻击者上传了脚本文件,服务器本身也不会受到影响。
-
使用安全设备防御,恶意文件千变万化,隐藏手法也不断推陈出新,对普通的系统管理员来说可以通过部署安全设备来帮助防御。