Struts2 S2-045

Posted by Yevon on March 26, 2017

对于Struts2漏洞,以前没怎么关注过,这不最近又出了个,借机了解下原理,在此做个记录。

官网漏洞信息:S2-045,另外还有一个类似的漏洞:S2-046

受影响版本:Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10

解决方法:升级至Struts 2.3.32 or Struts 2.5.10.1

原理

这次漏洞的原理是框架会对异常信息做OGNL表达式处理,因此存在执行恶意代码的威胁。而导致该漏洞的是Jakarta Multipart parser的文件上传模块,其在判断Content-Type时,如果不正确,则会抛出异常并且带上Content-Type的属性值,并做OGNL表达式处理。

验证

通过修改http请求的Content-Type值为如下参数,可验证是否存在该漏洞,如果Response Headers中包含了vul: vul,则表示存在,不包含则表示已修复或被拦截了。

Content-Type: %{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vul','vul')}.multipart/form-data

如果存在漏洞,则后台应该会报如下错误。

org.apache.commons.fileupload.FileUploadBase$InvalidContentTypeException: the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is %{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vul','vul')}.multipart/form-data
	at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:947)
	at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:310)
	at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:334)
	at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parseRequest(JakartaMultiPartRequest.java:189)
	at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.processUpload(JakartaMultiPartRequest.java:127)
	at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parse(JakartaMultiPartRequest.java:92)
	at org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper.<init>(MultiPartRequestWrapper.java:81)
	at org.apache.struts2.dispatcher.Dispatcher.wrapRequest(Dispatcher.java:803)
	at org.apache.struts2.dispatcher.ng.PrepareOperations.wrapRequest(PrepareOperations.java:134)
	at org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:91)
	...

分析

根据错误信息分析,首先Dispatcher类会判断Content-Type中是否包含multipart/form-data字符串,从而决定是否使用MultiPartRequestWrapper,由此可知只要包含了multipart/form-data字符串便符合要求。

//Class: org.apache.struts2.dispatcher.Dispatcher
//Line: 799
String content_type = request.getContentType();
if (content_type != null && content_type.contains("multipart/form-data")) {
    MultiPartRequest mpr = getMultiPartRequest();
    LocaleProvider provider = getContainer().getInstance(LocaleProvider.class);
    request = new MultiPartRequestWrapper(mpr, request, getSaveDir(servletContext), provider);
} else {
    request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);
}

然后FileUploadBase类会判断Context-Type的值,如果为null或者不是以multipart/开头的,则会抛出异常,同时带上Content-Type的值。

//Class: org.apache.commons.fileupload.FileUploadBase
//Line: 944
String contentType = ctx.getContentType();
if ((null == contentType)
        || (!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) {
    throw new InvalidContentTypeException(
            format("the request doesn't contain a %s or %s stream, content type header is %s",
                   MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));
}

最后异常捕获后JakartaMultiPartRequest类会将错误信息交由LocalizedTextUtil.findText方法build,最终会调用TextParseUtil.translateVariables(message, valueStack),而该方法支持OGNL表达式,且message便是之前异常抛出的错误信息,里面有Content-Type的值,因此这里有机会执行OGNL表达式。

//Class: com.opensymphony.xwork2.util.LocalizedTextUtil
//Line: 662
if (message != null) {
    MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);

    String msg = formatWithNullDetection(mf, args);
    result = new GetDefaultMessageReturnArg(msg, found);
}

最后的最后我稍微看了下2.3.32版本的处理方式,主要区别好像是最后一步,在该版本中,异常捕获后JakartaMultiPartRequest类仍会调用LocalizedTextUtil.findText方法,但并未传递异常信息,而是传了null,至于细节上并未详细了解,所以该见解仅作参考。

//Class: org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest

//Version: 2.3.16.3
//Line: 123
return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args);

//Version: 2.3.32
//Line: 126
return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null, args);

总结

漏洞真是无处不在啊~

参考资料