0x00 漏洞简述
对于Struts2的RCE漏洞来说,有三个关键点,一个是找到传入ognl的表达式的点,一个是绕过ognl里面的沙盒,一个是尝试理解那些“这tm都调用ognl的函数”。
在struts-default.xml中,Struts2默认处理multipart报文的解析器是jakarta。因此使用默认的jakarta的版本,便会存在该漏洞。
0x01 漏洞触发原理
在Strust2中,StrutsPrepareAndExecuteFilter类会默认处理content-type字段。(Jakarta为Struts2上传默认使用库,对上传数据进行解析。)
S2-045 RCE漏洞利用的正是上面这一点。
在Dispatcher.class中,只要content-type含有multipart/form-data便会进入检查流程。
public HttpServletRequest wrapRequest(HttpServletRequest request) throws IOException {
// don't wrap more than once
if (request instanceof StrutsRequestWrapper) {
return request;
}
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(), provider, disableRequestAttributeValueStackLookup);
} else {
request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);
}
return request;
}
在FileUploadBase.class类中的FileItemIteratorImpl函数
FileItemIteratorImpl(RequestContext ctx)
throws FileUploadException, IOException {
if (ctx == null) {
throw new NullPointerException("ctx parameter");
}
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));
}
...
由于ctx.getContentType()并没有获取到contentType,导致丢出一个异常,而这个异常就是拼接了恶意代码的异常。
而JakartaMultiPartRequest.class类的parse方法的buildErrorMessage函数会调用这个e。
public void parse(HttpServletRequest request, String saveDir) throws IOException {
try {
setLocale(request);
processUpload(request, saveDir);
} catch (FileUploadBase.SizeLimitExceededException e) {
if (LOG.isWarnEnabled()) {
LOG.warn("Request exceeded size limit!", e);
}
String errorMessage = buildErrorMessage(e, new Object[]{e.getPermittedSize(), e.getActualSize()});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
} catch (Exception e) {
if (LOG.isWarnEnabled()) {
LOG.warn("Unable to parse request", e);
}
String errorMessage = buildErrorMessage(e, new Object[]{});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
接下来的调用过程为
buildErrorMessage函数把e带入findText方法。
protected String buildErrorMessage(Throwable e, Object[] args) {
String errorKey = "struts.messages.upload.error." + e.getClass().getSimpleName();
if (LOG.isDebugEnabled()) {
LOG.debug("Preparing error message for key: [#0]", errorKey);
}
return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args);
}
LocalizedTextUtil类的findText方法会把e带入到getDefaultMessage函数内,然后进入buildMessageFormat函数。
if (message != null) {
MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);
String msg = formatWithNullDetection(mf, args);
result = new GetDefaultMessageReturnArg(msg, found);
}
先执行buildMessageFormat的TextParseUtil.translateVariables函数
public static String translateVariables(String expression, ValueStack stack) {
return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, null).toString();
}
然后对 $ 和 % 开头的内容进行搜索,然后都搜索出的内容带入 findValue。
接下来的内容就是绕过ognl的沙盒和拼接更好的利用输出了。
##0x02 Bypass Struts2 2.3.31 ognl沙盒
细心的同学肯定已经发现,此次bypass沙盒的方式显然跟以前不一样,利用中给出现了两次三元表达式,其中第一个三元表达式是进行bypass ognl沙盒的方式。其中运用了两处特性。
- ognl上下文中存在com.opensymphony.xwork2.ActionContext.container对象。
- com.opensymphony.xwork2.ognl.OgnlUtil的实例提供了过于强大的方法。
ActionContext.container实例存在getInstance方法。Container,之后便可以利用getInstance方法建立OgnlUtil对象。OgnlUtil类提供了获取和设置的Ognl API。Class OgnlUtil 而其中的 getExcludedClasses() 和 getExcludedPackageNamePatterns() 分别可以获取ognl的禁用的类和禁用的包,这俩方法获取的对象是的数据结构是set,之后调用clear方法便可以清除这些限制内容,导致沙盒失效。
0x03 防护方法
1,升级Struts至最新版
2,检查content-type内容