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>

application.yml

spring:
  mvc:
    view:
      prefix: /WEB-INF/jsp/
      suffix: .jsp
  web:
    resources:
      static-locations: classpath:/templates/


server:
  port: 8081

前置知识

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

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

表单中的enctype

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

处理文件时常用方法

separatorChar

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

public static final char separatorChar

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

separator

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

public static final String separator = "" + separatorChar;

其实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

常见文件上传方式

文件流上传

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

上传入口

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

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

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

springboot

spring:
  servlet:
    multipart:
      enabled: true
      # 单文件大小
      max-file-size: 100MB
      # 文件达到多少磁盘写入
      file-size-threshold: 4MB

springmvc

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--        需要与jsp中的pageEncoding配置一致,默认为iso-8859-1-->
    <property name="defaultEncoding" value="utf-8"/>
<!--   单文件大小,单位为字节10485700=100M-->
    <property name="maxUploadSize" value="10485700"/>
    <!--   文件达到多少磁盘写入-->
    <property name="maxInMemorySize" value="409600"/>
</bean>

上传入口

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

ServletFileUpload上传

基于Commons-FileUpload组件

依赖

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.2.2</version>
</dependency>

Springboot环境需关闭multipart

spring:
  servlet:
    multipart:
      enabled: false

创建步骤

  • 创建磁盘工厂: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>集合,每一个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();
        }
    }
}

上传入口

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

Servlet Part上传

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

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

方法 类型 是否可选 作用
fileSizeThershold int 当前数据量大于该值时,内容将被写入文件
location String 存放文件的路径
maxFileSize long 允许上传的文件最大值,默认为-1,表示没有限制
maxRequestSize long 针对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();
}

文件上传入口

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

文件上传漏洞

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

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

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

执行成功

wKg0C2NkvW2AcVW8AAAfHkS2rBw669.png

防御

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

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

重命名文件

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

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

绕过MIME检测后,可以通过白名单进行进一步的防御

wKg0C2NkvXuAE1x2AAFAx2P39Bc170.png

修改存储位置

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

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

最终代码

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

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

DiskFileItemFactory
@MultipartConfig
MultipartFile
File
upload
InputStream
OutputStream
write
fileName
filePath
免责声明:文章内容不代表本站立场,本站不对其内容的真实性、完整性、准确性给予任何担保、暗示和承诺,仅供读者参考,文章版权归原作者所有。如本文内容影响到您的合法权益(内容、图片等),请及时联系本站,我们会及时删除处理。查看原文

为您推荐