引言
  在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的。
java
  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 class MultipartAutoConfiguration { private final MultipartProperties multipartProperties; public MultipartAutoConfiguration(MultipartProperties multipartProperties) { this.multipartProperties = multipartProperties;
}
@Bean
@ConditionalOnMissingBean public MultipartConfigElement multipartConfigElement() { return this.multipartProperties.createMultipartConfig();
}
@Bean(name \= DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class) public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver \= new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily()); return multipartResolver;
}
}
``
org.springframework.web.multipart.support。StandardServletMultipartResolver`。查看对应的实现。
  可以看到其默认装配的解析器是
StandardServletMultipartResolver的处理方式
对应的spring-web组件版本为5.3.3
。使用StandardServletMultipartResolver
解析multipart请求的关键过程如下:
关键multipart请求的解析方法parseRequest
:
java
private void parseRequest(HttpServletRequest request)
{
try
{
Collection<Part> parts = request.getParts();
this.multipartParameterNames = new LinkedHashSet(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(), new StandardMultipartFile(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*=
解析文件名):
```java
public static ContentDisposition parse(String contentDisposition)
{
......
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 or ISO-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`方便后续程序调用:
java
private static class StandardMultipartFile
implements MultipartFile, Serializable
{
......
public String getOriginalFilename()
{
return this.filename;
}
......
```
实例化方法同样也没有对原始上传的filename进行检查/过滤,相关接口可以通过getOriginalFilename()方法获得对应的上传文件名,然后进行文件创建。
* 未做安全处理的文件上传
由于获取的fileName未进行安全处理,在使用File创建文件时,若路径处path写入../../穿越符号,是可以跨目录新建文件的:
java
File file = new File("path")
那么也就是说即使Springboot对jsp存在一定的支持限制,在特定情况下那么可以尝试上传定时任务进行权限获取,上传文件名为../../../../../../../../../var/spool/cron/root,结合前面百度到的demo成功反弹shell:
* 进行了后缀安全检查的文件上传
到这里针对Springboot任意文件上传的缺陷利用已经有一些眉目了。这里发现一个有趣的点。
针对任意文件上传,在进行业务开发的时候,常常会对后缀进行相关的白名单检查,如果上传非法后缀,那么拒绝对应的业务请求,例如如下代码:
```java
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”绕过对应的安全检查:
java


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

  其他版本的spring-web组件也是大同小异,例如4版本会通过`extractFilename()`方法进行multipart请求的处理再封装,同样没有处理目录穿越符的问题:
private String extractFilename(String contentDisposition, 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-framework/blob/master/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java 95-121行
可以看到其针对linux和windows的情况对multipart请求中的原始文件名进行了截断处理,防止了../../带来的目录穿越风险:
结语
上述的问题已经报告给了[email protected],官方回复如下:
因为某些原因暂不打算在StandardServletMultipartResolver上做更多的处理了。建议根据owasp提供的建议在实现上传业务时进行更多的安全检查。https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
根据前面提及的案例,在采用白名单方式检查文件扩展名的同时,建议对文件名进行二次处理,将其文件名随机命名,如UUID、GUID,不允许用户自定义。避免相关的安全风险。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论