JAVA审计-文件上传

前言

本篇记录关于java审计中文件上传部分。

0x01 Commons-FileUpload组件

利用这个组件上传是平时代码上传中遇到最多的。

关于Commons-FileUpload

Commons-FileUpload是Apache的一个组件,依赖于Commons-io,也是目前用的比较广泛的一个文件上传组件之一。

Spring MVCStruts2Tomcat等底层处理文件上传请求都是使用的这个库。

FileUpload上传的基本步骤:

  • 创建磁盘工厂:DiskFileItemFactory factory = new DiskFileItemFactory();

  • 创建处理工具:ServletFileUpload upload = new ServletFileUpload(factory);

  • 设置上传文件大小:upload.setFileSizeMax(3145728);

  • 接收全部内容:List items = upload.parseRequest(request);

Commons-FileUpload上传

示例servlet:

@Controller
@WebServlet("/upload")
public class FileUpload extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req,resp);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置文件上传路径
        String uploadDir = this.getServletContext().getRealPath("/upload/");
        File uploadFile = new File(uploadDir);
        //若不存在该路径则创建之
        if (!uploadFile.exists()&&!uploadFile.isDirectory()){
            uploadFile.mkdir();
        }
        String message = "";
        try {
            //创建一个磁盘工厂
            DiskFileItemFactory factory = new DiskFileItemFactory();
            //创建文件上传解析器
            ServletFileUpload fileupload = new ServletFileUpload(factory);
            //设置上传的文件大小
            fileupload.setFileSizeMax(3145728);
            //判断是否为multipart/form-data类型,为false则直接跳出该方法
            if (!fileupload.isMultipartContent(req)){
                return;
            }
            //使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
            List<FileItem> items = fileupload.parseRequest(req);

            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(uploadDir+File.separator+fileName);
                    //创建一个缓冲区
                    byte buffer[] = new byte[1024];
                    //判断输入流中的数据是否已经读完的标识
                    int length = 0;
                    //循环将输入流读入到缓冲区当中,(len=in.read(buffer))>0就表示in里面还有数据
                    while((length = is.read(buffer))>0){
                        //使用FileOutputStream输出流将缓冲区的数据写入到指定的目录(savePath + "\\" + filename)当中
                        fos.write(buffer, 0, length);
                    }
                    //关闭输入流
                    is.close();
                    //关闭输出流
                    fos.close();
                    //删除处理文件上传时生成的临时文件
                    item.delete();
                    message = "文件上传成功";
                }
            }
        } catch (FileUploadException e) {
            message = "文件上传失败";
            e.printStackTrace();
        }
        
    }
}

fileupload.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<form action="/jdk1_7vulns_war_exploded/upload" 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>

上面的代码里面对于文件处理只判断了文件是否为空,没有对文件类型进行限制,导致了任意文件上传:

image-20220302151951112

image-20220302152048641

image-20220302152102939

实际中主要是看对后缀的限制,还有一些代码通过new File(item.getContentType()).getName()得到contentType值判断,也是不严谨的。

0x02 SpringMVC MultipartResolver

使用MultipartResolver

MultipartResolver是专门处理文件上传的一个类

使用之前需要再pom中引入包:

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

MultipartResolver在上下文中没有被装配,需要手动装配MultipartResolver,在springMVC的配置文件dispatcher-Servlet.xml中进行以下的配置:

<!--文件上传配置-->
<bean id="multipartResolver"  class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
   <!-- 请求的编码格式,必须和jSP的pageEncoding属性一致,以便正确读取表单的内容,默认为ISO-8859-1 -->
   <property name="defaultEncoding" value="utf-8"/>
   <!-- 上传文件大小上限,单位为字节(10485760=10M) -->
   <property name="maxUploadSize" value="10485760"/>
   <property name="maxInMemorySize" value="40960"/>
</bean>

CommonsMultipartFile常用方法:

  • String getOriginalFilename():获取上传文件的原名
  • InputStream getInputStream():获取文件流
  • void transferTo(File dest):将上传文件保存到一个目录文件中

MultipartResolver+IO

示例代码:

@Controller
public class FileUploadController {

    //@RequestParam("file") 将name=file控件得到的文件封装成CommonsMultipartFile 对象
    @RequestMapping("/upload")
    public String fileUpload(@RequestParam("file") CommonsMultipartFile file , HttpServletRequest request) throws IOException {

        //获取文件名 : file.getOriginalFilename();
        String uploadFileName = file.getOriginalFilename();

        if ("".equals(uploadFileName)){
            return "redirect:/index.jsp";
        }
        System.out.println("上传文件名 : "+uploadFileName);

        //上传路径
        String path = request.getServletContext().getRealPath("/upload");
        //如果路径不存在,创建一个
        File realPath = new File(path);
        if (!realPath.exists()){
            realPath.mkdir();
        }
        System.out.println("上传文件地址:"+realPath);

        InputStream is = file.getInputStream(); 
        OutputStream os = new FileOutputStream(new File(realPath,uploadFileName));

        //读取写出
        int len=0;
        byte[] buffer = new byte[1024];
        while ((len=is.read(buffer))!=-1){
            os.write(buffer,0,len);
            os.flush();
        }
        os.close();
        is.close();
        return "redirect:/index.jsp";
    }

fileupload2.jsp

<form action="/jdk1_7vulns_war_exploded/upload2" enctype="multipart/form-data" method="post">
    <input type="file" name="file"/>
    <input type="submit" value="upload">
</form>

@RequestParam("file") CommonsMultipartFile file直接在前端传入过来的时候就封装成 CommonsMultipartFile 对象处理

image-20220302160905606

image-20220302160931733

CommonsMultipartFile+transferTo

@RequestMapping("/upload2")
public String  fileUpload2(@RequestParam("file") CommonsMultipartFile file, HttpServletRequest request) throws IOException {

   //上传路径保存设置
   String path = request.getServletContext().getRealPath("/upload");
   File realPath = new File(path);
   if (!realPath.exists()){
       realPath.mkdir();
  }
   //上传文件地址
   System.out.println("上传文件保存地址:"+realPath);

   //通过CommonsMultipartFile的方法直接写文件(注意这个时候)
   file.transferTo(new File(realPath +"/"+ file.getOriginalFilename()));

   return "redirect:/index.jsp";
}

0x03 Servlet Part

servlet3之后,可以不使用commons-fileupload、commons-io这两个jar包来处理文件上传,转而使用request.getParts()获取上传文件。

此外,servlet3还支持以注解的形式定义一些上传的属性。

image-20220302161822202

上传示例:

String savePath = request.getServletContext().getRealPath("/WEB-INF/uploadFile");
//Servlet3.0将multipart/form-data的POST请求封装成Part,通过Part对上传的文件进行操作。
Part part = request.getPart("file");//通过表单file控件(<input type="file" name="file">)的名字直接获取Part对象
//Servlet3没有提供直接获取文件名的方法,需要从请求头中解析出来
//获取请求头,请求头的格式:form-data; name="file"; filename="snmp4j--api.zip"
String header = part.getHeader("content-disposition");
//获取文件名
String fileName = getFileName(header);
//把文件写到指定路径
part.write(savePath+File.separator+fileName);

总结

对代码中的一些关键函数进行审计:

DiskFileItemFactory
@MultipartConfig
MultipartFile
File
upload
InputStream
OutputStream
write
fileName
filePath

或者有前端代码的话,可以结合multipart/form-data进行定位

参考

https://www.cnblogs.com/CoLo/p/15225367.html

https://www.cnblogs.com/nice0e3/p/13698256.html

https://github.com/proudwind/javasec_study/blob/master/java代码审计-文件操作.md

posted @ 2022-03-02 16:25  N0r4h  阅读(379)  评论(0编辑  收藏  举报