2018年4月,我向Apache Struts和Struts安全團隊中報告了一個新的遠程執(zhí)行代碼漏洞——CVE-2018-11776(S2-057),在做了某些配置的服務器上運行Struts,可以通過訪問精心構造的URL來觸發(fā)漏洞。
這一發(fā)現(xiàn)是我對Apache Struts的持續(xù)安全性研究的一部分。在這篇文章中,我將介紹我發(fā)現(xiàn)漏洞的過程以及如何利用以前的漏洞信息來獲取Struts內(nèi)部工作的原理,創(chuàng)建封裝Struts特定概念的QL查詢。運行這些查詢會高亮顯示有問題代碼的結果。這些工程都托管在GitHub上,后面我們也會向此存儲庫添加更多查詢語句和庫,以幫助Struts和其他項目的安全性研究。
映射攻擊面
許多安全漏洞都涉及了從不受信任的源(例如,用戶輸入)流向某個特定位置(sink)的數(shù)據(jù),并且數(shù)據(jù)采用了不安全的處理方式——例如,SQL查詢,反序列化,還有一些其他解釋型語言等等,QL可以輕松搜索此類漏洞。你只需要描述各種source和sink,然后讓DataFlow庫完成這些事情。對于特定項目,開始調(diào)查此類問題的一種好方法是查看舊版本軟件的已知漏洞。 這可以深入了解你想要查找的source和sink點。
這次漏洞發(fā)現(xiàn)過程中,我首先查看了RCE漏洞S2-032(CVE-2016-3081),S2-033(CVE-2016-3687)和S2-037(CVE-2016-4438)。 與Struts中的許多其他RCE一樣,這些RCE涉及不受信任的輸入被轉(zhuǎn)為OGNL表達式,允許攻擊者在服務器上運行任意代碼。 這三個漏洞特別有意思,它們不僅讓我們對Struts的內(nèi)部工作機制有了一些了解,而且這三個漏洞實際上是一樣的,還修復了三回!
這三個問題都是遠程輸入通過變量methodName作為方法的參數(shù)傳遞的造成的
OgnlUtil::getValue().
這里proxy有ActionProxy的類型,它是一個接口。 注意它的定義,除了方法getMethod()(在上面的代碼中用于賦值的變量methodName)之外,還有各種方法,如getActionName()和getNamespace()。 這些方法看起來像是會從URL返回信息,所以我就假設所有這些方法都可能返回不受信任的輸入。 (后面的文章中,我將深入研究我對這些輸入來自何處的調(diào)查。)
識別OGNL的 sink點
現(xiàn)在我們已經(jīng)識別并描述了一些不受信任的來源,下一步是為sink點做同樣的事情。 如前所述,許多Struts RCE涉及將遠程輸入解析為OGNL表達式。 Struts中有許多函數(shù)最終將它們的參數(shù)作為OGNL表達式; 對于我們在本文中開始的三個漏洞,使用了OgnlUtil :: getValue(),但是在漏洞S2-045(CVE-2017-5638)中,使用了TextParseUtil :: translateVariables()。 我們可以尋找用于執(zhí)行OGNL表達式的常用函數(shù),我感覺OgnlUtil :: compileAndExecute()和OgnlUtl :: compileAndExecuteMethod()看起來更有戲。
第一次嘗試
現(xiàn)在我們已經(jīng)在QL中定義了source和sink,我們可以在污點跟蹤查詢中使用這些定義。 通過定義DataFlow配置來使用DataFlow庫:
這里是我使用之前定義的isActionProxySource和isOgnlSink。
注意一下,我這里重載了isAdditionalFlowStep,這樣它可以允許我包含污染數(shù)據(jù)被傳播的額外步驟。 比如允許我將特定于項目的信息合并到流配置中。 例如,如果我有通過某個網(wǎng)絡層進行通信的組件,我可以在QL中描述那些各種網(wǎng)絡端的代碼是什么樣的,允許DataFlow庫跟蹤被污染的數(shù)據(jù)。
對于此特定查詢,我添加了兩個額外的流程步驟供DataFlow庫使用。 第一個:
它包括跟蹤標準Java庫調(diào)用,字符串操作等的標準QL TaintTracking庫步驟。第二個添加是一個近似值,允許我通過字段訪問跟蹤污點數(shù)據(jù):
也就是說如果將字段賦了某個受污染的值,那么只要兩個表達式都由相同類型的方法調(diào)用,對該字段的訪問也將被視為污染。看下面的例子:
從上面看出,bar()中this.field的訪問可能并不總是受到污染。 例如,如果在bar()之前未調(diào)用foo()。 因此,我們不會在默認的DataFlow :: Configuration中包含這個步驟,因為無法保證數(shù)據(jù)始終以這種方式流動,但是,對于挖漏洞,我覺得加上這個很有用。在后面的帖子中,我將分享一些類似于這個的其他流程步驟,這些步驟對于找bug很有幫助,但由于類似的原因,默認情況下是不包含這些步驟的。
初始結果和細化查詢
我在最新版本的源代碼上跑了一下QL,發(fā)現(xiàn)因S2-032,S2-033和S2-037仍然被標記了。 這些漏洞明明已經(jīng)被修復了,為什么還是會報問題呢?
經(jīng)過分析,我們覺得應該是雖然最初通過過濾輸入來修復漏洞,但是在S2-037之后,Struts團隊決定通過調(diào)用OgnlUtil :: getMalue()替換對OgnlUtil :: getMalue()的調(diào)用來修復它。
callMethod()封裝了compileAndExecuteMethod():
compileAndExecuteMethod()在執(zhí)行之前對表達式執(zhí)行額外檢查:
這意味著我們實際上可以從我們的sink點中刪除compileAndExecuteMethod()。
在重新運行查詢后,高亮顯示對getMethod()作為sink的調(diào)用的結果消失了。 但是,仍然有一些結果高亮顯示了DefaultActionInvocation.java中的代碼,這些代碼被認為是固定的,例如對getActionName()的調(diào)用,并且數(shù)據(jù)路徑從此處到compileAndExecute()并不是很明顯。
路徑探索和進一步查詢細化
為了搞清楚為什么這個結果被標記,需要能夠看到DataFlow庫用來產(chǎn)生這個結果的每個步驟。 QL允許編寫特殊的路徑問題查詢,這些查詢可生成可逐節(jié)點探索的可變長度路徑,DataFlow庫允許編寫輸出此數(shù)據(jù)的查詢。
在撰寫這篇博客的時候,LGTM本身沒有關于路徑問題查詢的路徑探索UI,因此用了另一個Semmle應用程序:QL for Eclipse。這是一個Eclipse插件,剛好可以滿足我們這里的需求,允許完成污點跟蹤中的各個步驟。它不僅可以在LGTM.com上對開源項目進行離線分析,還可以為提供更強大的開發(fā)環(huán)境。可以在semmle-security-java目錄下的Semmle / SecurityQueries Git存儲庫中找到以下查詢。按照README.md文件中的說明在Eclipse插件中運行。下文將貼出部分運行的截圖。
首先,在initial.ql中運行查詢。在QL for Eclipse中,從DefaultActionInvocation.java中選擇結果后,您可以在Path Explorer窗口中看到從源到接收器的詳細路徑。
在上圖中可以看出,經(jīng)過幾個步驟后,調(diào)用getActionName()返回的值會流入到pkg.getActionConfigs()返回的對象的get()方法的參數(shù)中:
點擊下一步,key到了ValueStackShadowMap :: get()方法:
事實證明,因為pkg.getActionConfigs()返回一個Map,而ValueStackShadowMap實現(xiàn)了Map接口,所以理論上pkg.getActionConfigs()返回的值可能是ValueStackShadowMap的一個實例。 因此,QL DataFlow庫顯示了從變量chainedTo到類ValueStackShadowMap中的get()實現(xiàn)的潛在流程。 實際上,ValueStackShadowMap類屬于jasperreports插件,該類的實例僅在幾個地方創(chuàng)建。因此我覺得問題應該不在ValueStackShadowMap :: get(),我通過在DataFlow :: Configuration中添加一個barrier來排除這種結果:
這里的意思是如果污染數(shù)據(jù)流入ValueStackShadowMap的get()或containsKey()方法,那么就不要繼續(xù)跟蹤它。 (我在這里添加了containsKey()方法,因為它也有同樣的問題。)
又為ActionMapping :: toString()添加了barrier之后(因為在任意對象上調(diào)用toString()時出問題),重新運行查詢,只留下了部分結果。 當然你也可以嘗試使用Eclipse插件來顯示污點路徑。
發(fā)現(xiàn)漏洞
只有10對source和sink,很容易通過手工檢查這些是否是真正的問題。 通過一些路徑,看出有些路徑是無效的,所以我又在查詢中添加了一些barrier來過濾掉這些路徑。 最后的結果比較有意思。
以ServletActionRedirectResult.java中的源代碼為例:
在第一步中,調(diào)用getNamespace()的source通過變量名稱空間流入ActionMapping構造函數(shù)的參數(shù)中:
繼續(xù)跟這些步驟,看到getUriFromActionMapping()返回一個URL字符串,該字符串使用構造的ActionMapping中的命名空間。 然后通過變量tmpLocation流入setLocation()的參數(shù):
然后setLocation()在超類StrutsResultSupport中設置location:
然后代碼在ServletActionResult上調(diào)用execute():
將location字段傳遞給對conditionalParse():
conditionalParse()然后將location傳遞給translateVariables(),它將param轉(zhuǎn)化為引擎蓋下的OGNL表達式:
所以看起來當在ServletActionRedirectResult中沒有設置namespace參數(shù)時,代碼從ActionProxy獲取命名空間,然后將其作為OGNL表達式。 為了驗證這個想法,我通過以下方法替換了showcase應用程序中的一個配置文件(例如struts-actionchaining.xml)中的struts標記:
然后我在本地運行showcase應用程序,訪問了一個旨在觸發(fā)此漏洞的URL并執(zhí)行shell命令以在我的計算機上打開計算器應用程序。
彈出計算器了(中間還花了一些時間繞過OGNL沙箱)。現(xiàn)在我暫時不提供進一步的細節(jié),后面會發(fā)出來。
不單單這一處,來自ActionChainResult,PostbackResult和ServletUrlRenderer的不可信來源都能彈出計算器! PortletActionRedirectResult中的那個可能也可以,但我沒有測試。 四個RCE足以證明問題的嚴重性。
結論
在這篇文章中,我已經(jīng)展示了通過使用已知(過去)的漏洞來幫助構建應用程序的污點模型,然后由 QL DataFlow庫找新的漏洞。特別是通過研究Struts中之前的三個RCE,最終找到了四個(也可能是五個)!
鑒于S2-032,S2-033和S2-037都是在短時間內(nèi)被發(fā)現(xiàn)和修復的,安全研究人員清楚地研究了S2-032用以尋找類似問題并發(fā)現(xiàn)S2-033和S2-037。這里就有問題了:我發(fā)現(xiàn)的漏洞(S2-057)也來自類似的污染源,為什么安全研究人員和供應商之前沒發(fā)現(xiàn)?在我看來,這是因為S2-032,S2-033和S2-037之間的相似性在某種意義上是局部的,因為它們都出現(xiàn)在源代碼中的相似位置(全部在Rest插件中)。 S2-057和S2-032之間的相似性處于更加語義的層面。它們由受污染的源鏈接,而不是源代碼的位置,因此任何能夠成功找到這樣的變體的軟件或工具都需要能夠在整個代碼庫中執(zhí)行這種語義分析,就像我現(xiàn)在可演示的QL。
如果你認為我的這些發(fā)現(xiàn)只是運氣好,因為我假設ActionProxy中的命名空間字段已經(jīng)被污染了,那么請繼續(xù)關注下一篇文章,我會展示更多的細節(jié)問題,并從傳入的HttpRequestServlet本身開始,從“第一原則”開始進行一些污點跟蹤。我還將從我的“漏洞狩獵工具箱”中分享一些工具,以及一些改進查詢的一般提示。在這樣做的過程中,QL還捕獲了漏洞S2-045!