
1. 萬年漏洞王又出事了。。。
17年三月份,Struts2再一次被爆出一個(gè)嚴(yán)重的漏洞S2-045,上傳文件時(shí)可能存在RCE(Remote Code Execution)。由于涉及到的文件上傳模塊(Jakarta Multipart parser)是Struts2的默認(rèn)配置,所以這一漏洞影響范圍非常廣泛。上一次影響范圍如此大的漏洞是13年的s2-013。而且從Struts2的Security Bulletins列表中可以看到,remote command execution和Remote Code Execution這樣的字眼經(jīng)常出現(xiàn),這篇博客在分析最近這次漏洞原理的基礎(chǔ)上,也會探究一個(gè)重要的問題:為什么Struts2總是出現(xiàn)漏洞?
2. S2-045原因簡析
2.1 漏洞觸發(fā)原理
這個(gè)漏洞的主要涉及到的是文件上傳功能,我們知道在上傳文件的時(shí)候,一般請求對象request的header中的contentType字段是multipart/form-data或multipart/mixed,如果有人構(gòu)造一個(gè)錯(cuò)誤的contentType,那么Struts2會拋出異常,下面是在parse request對象的時(shí)候可能拋出異常的部分:
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));
}
其實(shí)這個(gè)驗(yàn)證流程沒有問題,問題出在拋出異常后的處理。Struts2中使用的是JakartaMultiPartRequest用于封裝文件上傳請求,下面是JakartaMultiPartRequest的parse方法,該方法主要用于實(shí)現(xiàn)文件上傳,其中包括上面說到的請求內(nèi)容的驗(yàn)證過程,如果如上所述contentType出現(xiàn)錯(cuò)誤,那么processUpload會拋出InvalidContentTypeException異常,如下:
public void parse(HttpServletRequest request, String saveDir) throws IOException {
try {
setLocale(request);
processUpload(request, saveDir);//驗(yàn)證出錯(cuò),拋出InvalidContentTypeException異常
} catch (FileUploadBase.SizeLimitExceededException e) {
//process SizeLimitExceededException
} catch (Exception e) {
//InvalidContentTypeException在這里被catch住
String errorMessage = buildErrorMessage(e, new Object[]{});//漏洞源
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
}
}
重點(diǎn)在于struts2在catch塊中對于異常的處理過程。可見這里調(diào)用了buildErrorMessage對異常進(jìn)行處理,返回一個(gè)String類型的errorMessage。下面是buildMessage中的簡易調(diào)用關(guān)系圖:

可以看到,最終的解析任務(wù)落到了ValueStack對象上。ValueStack組件是Struts2中非常重要的組成部分,該組件的功能就是作為一個(gè)表達(dá)式引擎,解決MVC框架中普遍存在的數(shù)據(jù)轉(zhuǎn)換問題。其默認(rèn)實(shí)現(xiàn)是基于Ognl的OgnlValueStack。Ognl是一個(gè)功能強(qiáng)大的表達(dá)式引擎,潛在的惡意代碼就是在該表達(dá)式引擎中被執(zhí)行。下面重點(diǎn)分析下Struts2中使用的表達(dá)式引擎OGNL。
2.2 Ognl:這個(gè)鍋俺不背
Struts2出現(xiàn)的漏洞大多與RCE(Remote Code Execution)有關(guān),request中都是文本形式的數(shù)據(jù),怎么會被執(zhí)行了呢?這就不得不說Ognl了。
2.2.1 什么是Ognl?
OGNL(Object-Graph Navigation Language)是一個(gè)開源的表達(dá)式引擎,我們可以通過Ognl來存取Java對象
的任意屬性,也可以調(diào)用Java對象的方法。下面是表達(dá)式引擎的簡單示意圖:
表達(dá)式引擎示意圖
2.2.2 為什么需要Ognl?
要弄清楚這個(gè)問題,還得回到MVC模式本身。MVC模式面臨著數(shù)據(jù)在不同組件中的不斷流轉(zhuǎn)的事實(shí),比如:在Model中數(shù)據(jù)表現(xiàn)形式為Java對象,而Java世界中具有豐富的數(shù)據(jù)類型(List、Map、Set等)??墒窃赩iew層中,數(shù)據(jù)表現(xiàn)為字符串,其目的僅僅是為了展示內(nèi)容。這就造成了數(shù)據(jù)形式不匹配的問題,那么怎么解決呢?這就需要一個(gè)"翻譯":表達(dá)式引擎。如:Ognl,SpringEL等。
下面通過幾個(gè)簡單的實(shí)例看下通過Ognl可以做什么?
首先自定義OgnlExpression,對Ognl的部分功能進(jìn)行封裝。如下:
public class OgnlExpression {
private Object expression;
public OgnlExpression(String expressionString) throws OgnlException{
expression = Ognl.parseExpression(expressionString);
}
public Object getExpression(){
return this.expression;
}
public Object getValue( OgnlContext context, Object rootObject )
throws OgnlException
{
return Ognl.getValue( getExpression(), context, rootObject );
}
public void setValue( OgnlContext context, Object rootObject, Object value )
throws OgnlException
{
Ognl.setValue(getExpression(), context, rootObject, value);
}
}
示例一,利用Ognl訪問對象屬性、調(diào)用對象方法:
String expression = "[1].toCharArray()[2]";
OgnlExpression ognlExpr = new OgnlExpression(expression);
String arr[] = {"struts2","ognl","action"};
OgnlContext context = new OgnlContext();
context.put("arr", arr);
System.out.println(ognlExpr.getValue(context, arr));
輸出:
