浅谈 Springboot 中的文件上传

sugar 2021-02-03
专栏 - 观点观察 发布于 2021-02-03 17:08:08 阅读 116 评论 0

引言

在JavaWeb应用中,任意文件上传一直是关注的重点,攻击者通过上传恶意jsp文件,可以获取服务器权限。但是在Springboot框架对JSP解析存在一定的限制。

Spring官方原文如下,大概意思是jsp对内嵌的容器的支持不太友好,推荐使用thymeleaf这类的模版引擎进行渲染。

图片

那么针对Springboot应用,即使存在任意文件上传缺陷,按照传统的思路直接上传jsp文件,也是无法达到理想的效果的。

下面通过查看其具体的实现方式来看看有没有相关的利用思路,同时在日常项目开发中应该注意些什么。

Springboot文件上传的实现

首先看看在Springboot中如何实现文件上传功能,在网上找了个教程,Controller的代码如下,Spring会自动解析multipart/form-data请求,将multipart中的对象封装到MultipartRequest对象中:

@RequestMapping(value={"/uploadFile"},method={RequestMethod.POST})
public String uploadFile(MultipartFile file,String type,HttpServletResponse
response) throws
Exception{
        String UPLOADED_FOLDER="/resource/upload/";
        if(!file.isEmpty()){
                String
path = UPLOADED_FOLDER + file.getOriginalFilename();
                File
targetFile = new
File(path);
                FileUtils.inputStreamToFile(file.getInputStream(),targetFile);
                        ......
                        ......
}
}

大致是通过getOriginalFilename()方法获取文件名,然后使用File对象创建对应的文件。接下来看看Springboot是如何解析multipart请求并封装OriinalFilename的。

SpringBoot在

MultipartAutoConfiguration自动装配了MultipartResolver来对multipart请求进行解析:

@Configuration
@ConditionalOnClass({ Servlet.class,StandardServletMultipartResolver.class,
        MultipartConfigElement.class})
@ConditionalOnProperty(prefix \= "spring.http.multipart", name= "enabled", matchIfMissing =true)
@EnableConfigurationProperties(MultipartProperties.class) public classMultipartAutoConfiguration { private final MultipartPropertiesmultipartProperties; public MultipartAutoConfiguration(MultipartPropertiesmultipartProperties) { this.multipartProperties =multipartProperties;
    }
    @Bean
    @ConditionalOnMissingBeanpublic MultipartConfigElement multipartConfigElement() { returnthis.multipartProperties.createMultipartConfig();
    }
    @Bean(name \=DispatcherServlet.MULTIPART\_RESOLVER\_BEAN\_NAME)
   @ConditionalOnMissingBean(MultipartResolver.class) publicStandardServletMultipartResolver multipartResolver() {
        StandardServletMultipartResolvermultipartResolver \= new StandardServletMultipartResolver();
       multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());return multipartResolver;
   }
}

可以看到其默认装配的解析器是org.springframework.web.multipart.support。StandardServletMultipartResolver。查看对应的实现。

StandardServletMultipart Resolver的处理方式

对应的spring-web组件版本为5.3.3。使用StandardServletMultipartResolver解析multipart请求的关键过程如下:

关键multipart请求的解析方法parseRequest

private void parseRequest(HttpServletRequestrequest)
  {
    try
    {
      Collection<Part>parts = request.getParts();
      this.multipartParameterNames = newLinkedHashSet(parts.size());
      MultiValueMap<String,MultipartFile> files = new LinkedMultiValueMap(parts.size());
      for (Part part : parts)
      {
        String headerValue =part.getHeader("Content-Disposition");
        ContentDisposition disposition =ContentDisposition.parse(headerValue);
        String filename =disposition.getFilename();
        if (filename != null)
        {
          if((filename.startsWith("=?")) && (filename.endsWith("?="))){
            filename =MimeDelegate.decode(filename);
          }
          files.add(part.getName(), newStandardMultipartFile(part, filename));
        }
        else
        {
         this.multipartParameterNames.add(part.getName());
        }
      }
      setMultipartFiles(files);
    }
    catch (Throwable ex)
    {
      handleParseFailure(ex);
    }
  }

主要的解析方法在

org.springframework.http.ContentDisposition的parse方法,在这里对相关的http内容进行了处理,获取文件名的关键内容如下。

如果传入的multipart请求无法直接使用filename=解析出文件名,Spring还会使用content-disposition解析一次(使用filename*=解析文件名):

public static ContentDisposition parse(StringcontentDisposition)
  {
    ......
    for (int i = 1; i < parts.size();i++)
    {
      String part =(String)parts.get(i);
      int eqIndex =part.indexOf('=');
      if (eqIndex!= -1)
      {
        String attribute =part.substring(0, eqIndex);

        String value =(part.startsWith("\"", eqIndex + 1)) &&(part.endsWith("\"")) ? part.substring(eqIndex + 2,part.length() - 1) : part.substring(eqIndex + 1);
        if(attribute.equals("name"))
        {
          name = value;
       }
        else if(attribute.equals("filename*"))
        {
          int idx1 =value.indexOf('\'');
          int idx2 = value.indexOf('\'',idx1 + 1);
          if ((idx1 != -1) &&(idx2 != -1))
          {
            charset =Charset.forName(value.substring(0, idx1).trim());
           Assert.isTrue((StandardCharsets.UTF_8.equals(charset)) ||(StandardCharsets.ISO_8859_1.equals(charset)), "Charset should be UTF-8 orISO-8859-1");

            filename =decodeFilename(value.substring(idx2 + 1), charset);
          }
          else
          {
            filename =decodeFilename(value, StandardCharsets.US_ASCII);
          }
        }
        else if((attribute.equals("filename")) && (filename ==null))
        {
          filename = value;
        }

这里发现一个点,整个过程没有对类似…/的路径进行检查/过滤,获取文件名后会实例化StandardMultipartFile方便后续程序调用:

private static classStandardMultipartFile
    implements MultipartFile,Serializable
  {
    ......

    public StringgetOriginalFilename()
    {
      return this.filename;
    }
    ......

实例化方法同样也没有对原始上传的filename进行检查/过滤

相关接口可以通过getOriginalFilename()方法获得对应的上传文件名,然后进行文件创建。

未做安全处理的文件上传

由于获取的fileName未进行安全处理,在使用File创建文件时,若路径处path写入…/…/穿越符号,是可以跨目录新建文件的:

File file = new File("path")

浅谈Springboot中的文件上传

那么也就是说即使Springboot对jsp存在一定的支持限制,在特定情况下那么可以尝试上传定时任务进行权限获取。

上传文件名为…/…/…/…/…/…/…/…/…/var/spool/cron/root,结合前面百度到的demo成功反弹shell:

浅谈Springboot中的文件上传

进行了后缀安全检查的文件上传

到这里针对Springboot任意文件上传的缺陷利用已经有一些眉目了。这里发现一个有趣的点。

针对任意文件上传,在进行业务开发的时候,常常会对后缀进行相关的白名单检查,如果上传非法后缀,那么拒绝对应的业务请求,例如如下代码:

if(!file.isEmpty()){
        String Filename =
file.getOriginalFilename();
        String suffix =
originalFilename.substring(Filename.lastIndexOf("."));
        if(!".xlsx".equals(suffix)&&!".xls".equals(suffix)){
                throw
new Exception("非法请求,请导入excel文件");
        }
byte[] bytes =
file.getBytes();
        String path =ULOADED_FOLDER +
Filename;
}

在进行文件上传时进行了后缀检查,如果不是xlsx或者xls后缀的话,拒绝请求。因为/etc/cron.d/目录下的文件可以任意后缀命名,那么此时可以上传文件名为“…/…/…/…/…/…/etc/cron.d/test.xls”绕过对应的安全检查:

浅谈Springboot中的文件上传

浅谈Springboot中的文件上传

上传成功后,本地监听端口等待定时任务执行,成功反弹 shell,获取服务器权限:

浅谈Springboot中的文件上传

其他版本的spring-web组件也是大同小异,例如4版本会通过extractFilename()方法进行multipart请求的处理再封装,同样没有处理目录穿越符的问题:

private String extractFilename(StringcontentDisposition, String key)
  {
    if (contentDisposition == null){
      return null;
    }
    int startIndex =contentDisposition.indexOf(key);
    if (startIndex == -1) {
      return null;
    }
    String filename =contentDisposition.substring(startIndex + key.length());
    if(filename.startsWith("\""))
    {
      int endIndex =filename.indexOf("\"", 1);
      if (endIndex != -1) {
        return filename.substring(1,endIndex);
      }
   }
    else
    {
      int endIndex =filename.indexOf(";");
     if (endIndex != -1) {
        return filename.substring(0,endIndex);
      }
    }
    return filename;
  }

综上,关于Springboot的文件上传可以通过结合目录遍历的方式尝试利用,当然也需要满足一定的利用条件,例如对跨目录具有写权限、未重命名文件名等。

同样的SpringMVC也加载了默认的解析器,一般是CommonsMultipartResolver,查看其对应的处理方式进行对比。

CommonsMultipartResolver的处理方式

解析部分就不细看了,直接查看getOriginalFilename()

https://github.com/spring-projects/spring-... 95-121行

可以看到其针对linux和windows的情况对multipart请求中的原始文件名进行了截断处理,防止了…/…/带来的目录穿越风险:

浅谈Springboot中的文件上传

结语

上述的问题已经报告给了

security@pivotal.io,官方回复如下:

图片

因为某些原因暂不打算在

StandardServletMultipartResolver上做更多的处理了。

建议根据owasp提供的建议,在实现上传业务时进行更多的安全检查。

https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html

原创:tkswifty SecIN技术平台
原文链接:https://mp.weixin.qq.com/s/wPgdnyv57qBwkHV...

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!
请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!