浅谈 Springboot 中的文件上传
引言
在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对jsp存在一定的支持限制,在特定情况下那么可以尝试上传定时任务进行权限获取。
上传文件名为…/…/…/…/…/…/…/…/…/var/spool/cron/root,结合前面百度到的demo成功反弹shell:
进行了后缀安全检查的文件上传
到这里针对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”绕过对应的安全检查:
上传成功后,本地监听端口等待定时任务执行,成功反弹 shell,获取服务器权限:
其他版本的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请求中的原始文件名进行了截断处理,防止了…/…/带来的目录穿越风险:
结语
上述的问题已经报告给了
security@pivotal.io
,官方回复如下:
因为某些原因暂不打算在
StandardServletMultipartResolver上做更多的处理了。
建议根据owasp提供的建议,在实现上传业务时进行更多的安全检查。
https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
原创:tkswifty SecIN技术平台
原文链接:https://mp.weixin.qq.com/s/wPgdnyv57qBwkHV...
