原创|StandardServletMultipartResolver与文件上传bypass的那些事儿

admin 2022年5月16日09:41:46评论52 views字数 7053阅读23分30秒阅读模式

点击蓝字




关注我们



在JavaWeb应用中,任意文件上传一直是关注的重点,攻击者通过上传恶意jsp文件,可以获取服务器权限。作为Java生态中最常使用的Spring框架,在进行文件上传解析时主要是这两个解析器:
  • CommonsMultipartResolver(主要是基于Apache commons fileupload库)
  • StandardServletMultipartResolver
CommonsMultipartResolver有很多师傅都分析过了,也有了很多有意思的trick。
例如我是killer师傅提到了在 filename= 1.jsp的filename字符左右可以加上⼀些空⽩字符 %20%09 %0a %0b %0c %0d %1c %1d %1e %1f ,导致waf匹配不到我们上传⽂件名,⽽我们上传依然可以解析,达到绕过检测的效果。再者还有师傅提到了使用QP编码进行处理,如将测试.jsp进行QP编码处理后为=?UTF-8?Q?=E6=B5=8B=E8=AF=95=2Ejsp?=来达到绕过的效果。
本文主要围绕另一个解析器StandardServletMultipartResolver,看看有没有什么bypass waf的思路。测试代码如下:
@PostMapping(path = "/FileUpload")
public String log4j(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return "上传失败,请选择文件";
}

String fileName = file.getOriginalFilename();
String filePath = "/tmp/";
File dest = new File(filePath + fileName);
try {
file.transferTo(dest);
return "上传成功,fileName:"+file.getOriginalFilename();
} catch ( IOException e) {

}
return "上传失败!";
}

原创|StandardServletMultipartResolver与文件上传bypass的那些事儿


StandardServletMultipartResolver解析


对于一个正常的waf来说,最常见的思路是截取到filename=file_name.jsp,发现扩展名为jsp,接着进行拦截,那么目标很明确,那就是waf解析出的filename不出现jsp关键字,并且后端程序在验证扩展名的时候会认为这是一个jsp文件。

filename参数一般出现在Content-Dispostion:

Content-Disposition: form-data; name="key"; filename="file.jsp"
这里主要看看StandardServletMulipartResolver是怎么解析Content-Dispostion的。由于Spring4.x与Spring5.x的代码不一致,这里分别进行分析。
StandardServletMultipartResolver中关键multipart请求的解析方法org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest,这个是一致的:
  • Spring 4.x
关键multipart请求的解析方法parseRequest:(spring-web-4.3.30.RELEASE),主要是在extractFilename进行文件名的获取,如果获取不到filename的话则调用extractFilenameWithCharset()进行filename的获取:
private void parseRequest(HttpServletRequest request) {
try {
Collection<Part> parts = request.getParts();
this.multipartParameterNames = new LinkedHashSet<String>(parts.size());
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<String, MultipartFile>(parts.size());
for (Part part : parts) {
String disposition = part.getHeader(CONTENT_DISPOSITION);
String filename = extractFilename(disposition);
if (filename == null) {
filename = extractFilenameWithCharset(disposition);
}
if (filename != null) {
files.add(part.getName(), new StandardMultipartFile(part, filename));
}
else {
this.multipartParameterNames.add(part.getName());
}
}
setMultipartFiles(files);
}
catch (Throwable ex) {
throw new MultipartException("Could not parse multipart servlet request", ex);
}
}
 extractFilename主要是进行substring的切割:

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

extractFilenameWithCharset()主要是对filename*=参数进行处理:

private static final String FILENAME_WITH_CHARSET_KEY = filename*= ;
private String extractFilenameWithCharset(String contentDisposition) {
String filename = extractFilename(contentDisposition, FILENAME_WITH_CHARSET_KEY);
if (filename == null) {
return null;
}
......
return filename;
}

  • Spring 5.x

关键multipart请求的解析方法parseRequest(spring-web-5.3.16),主要的解析方法在org.springframework.http.ContentDisposition的parse方法,在这里对相关的http内容进行了处理:

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(HttpHeaders.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);
}
}

定位到具体的解析方法后,看看具体的解析方式,看看有什么waf bypass的思路。

filename*=解析

参考https://datatracker.ietf.org/doc/html/rfc6266 4.3小节:

原创|StandardServletMultipartResolver与文件上传bypass的那些事儿


按照 RFC标准,在 filename 和 filename* 同时出现的情况下,按照 RFC应当忽略 filename=,解析filename*=,并且会解析如下编码格式(=后面是编码方式,再接着是两个单引号,再接着是使用前面指定编码编码后的文件名)

Content-Disposition: attachment; filename*= UTF-8''1.jsp

StandardServletMultipartResolver实现了这一个标准,分别查看4.x和5.x的具体实现。

  • Spring4.x处理方式

前面提到了extractFilenameWithCharset()主要是对filename*=参数进行处理:

private String extractFilenameWithCharset(String contentDisposition) {
String filename = extractFilename(contentDisposition, FILENAME_WITH_CHARSET_KEY);
if (filename == null) {
return null;
}
int index = filename.indexOf( ' );
if (index != -1) {
Charset charset = null;
try {
charset = Charset.forName(filename.substring(0, index));
}
catch (IllegalArgumentException ex) {
// ignore
}
filename = filename.substring(index + 1);
// Skip language information..
index = filename.indexOf( '
);
if (index != -1) {
filename = filename.substring(index + 1);
}
if (charset != null) {
filename = new String(filename.getBytes(US_ASCII), charset);
}
}
return filename;
}

获取到filename*=后的内容后,首先切割第一个',通过Charset获取对应的编码方式,然后再切割第二个' 后的内容,并根据前面的编码方式进行解码操作,最后返回对应的filename。可以看到实际上两个' 之间是可以任意填充内容的(单引号之间的内容在实际解析时会被忽略掉):

原创|StandardServletMultipartResolver与文件上传bypass的那些事儿


  • Spring5.x处理方式

与Spring4的方式类似,对于filename*=的内容,例如传入的UTF-8'aaa'1.jsp会被解析成UTF-8编码,最终的文件名为1.jsp,而aaa则会被丢弃,主要在ContentDisposition.parse方法进行解析:

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(UTF_8.equals(charset) || ISO_8859_1.equals(charset),
"Charset should be UTF-8 or ISO-8859-1");
filename = decodeFilename(value.substring(idx2 + 1), charset);
}
else {
// US ASCII
filename = decodeFilename(value, StandardCharsets.US_ASCII);
}
}

MIME编码

MIME定义了两种编码方法,其中一种是BASE64,另一种是Quote-Printable,即QP编码。在Spring的MultipartResolver中也有对应的实现。

  • QP编码

前面提到了CommonsMultipartResolver可以使用QP编码进行处理,如将测试.jsp进行QP编码处理后为=?UTF-8?Q?=E6=B5=8B=E8=AF=95=2Ejsp?=来达到绕过的效果。

对于StandardMultipartHttpServletRequest解析器,在Spring 5.x实现了QP解码,若解析时文件名是=?开始?=结尾,会调用javax.mail库的MimeDelegate解析QP编码,但是要注意的是,javax.mail 库不是 JDK 自带的,必须自行引包,如果不存在该包也将无法解析 :

if (filename != null) {
if (filename.startsWith( =? ) && filename.endsWith( ?= )) {
filename = MimeDelegate.decode(filename);
}
files.add(part.getName(), new StandardMultipartFile(part, filename));
}

  • BASE64编码

从spring-web-5.3.4开始,在ContentDisposition.parse方法中进行了实现。在解析filename的时候多了一个正则处理:

private final static Pattern BASE64_ENCODED_PATTERN =
Pattern.compile( =\?([0-9a-zA-Z-_]+)\?B\?([+/0-9a-zA-Z]+=*)\?= );

具体代码如下:

当filename的值以=?开头时,会进入BASE64_ENCODED_PATTERN的正则匹配中,大致的可以知道需要匹配的内容应该是=?编码方式?B?编码内容?= :

else if (attribute.equals( filename ) && (filename == null)) {
if (value.startsWith( =? ) ) {
Matcher matcher = BASE64_ENCODED_PATTERN.matcher(value);
if (matcher.find()) {
String match1 = matcher.group(1);
String match2 = matcher.group(2);
filename = new String(Base64.getDecoder().decode(match2), Charset.forName(match1));
}
else {
filename = value;
}
}
else {
filename = value;
}
}

例如1.jsp经过上述处理后如下:

name= content ;filename= =?utf-8?B?MS5qc3A=?=

原创|StandardServletMultipartResolver与文件上传bypass的那些事儿

可以看到整个filename里不包含jsp等关键字,并且成功上传文件。

综上,在使用StandardServletMultipartResolver进行上传解析时,可以通过相应的编码来尝试进行waf bypass。

原创|StandardServletMultipartResolver与文件上传bypass的那些事儿



往期推荐


原创 |CodeQL与AST之间联系
原创 | GitHub Java CodeQL CTF
原创 | 浅谈Log4j2在Springboot的检测-2

原文始发于微信公众号(SecIN技术平台):原创|StandardServletMultipartResolver与文件上传bypass的那些事儿

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月16日09:41:46
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   原创|StandardServletMultipartResolver与文件上传bypass的那些事儿https://cn-sec.com/archives/1005338.html

发表评论

匿名网友 填写信息