xwork修補(bǔ)漏洞的悲劇
在漏洞發(fā)現(xiàn)者發(fā)布的POC中,并不能影響xwork 2.1.2之前的一些版本(這個(gè)版本之前的一些版本,下文會(huì)統(tǒng)稱為老版本,之后的叫做新版),例如struts 2.0.14(就是struts修補(bǔ)了N個(gè)高危漏洞后的第一個(gè)版本,最常用的版本)其實(shí)是不能打的,本文會(huì)分析這個(gè)漏洞的起因,和結(jié)果,也會(huì)給出通殺POC的思路。
本文希望看懂文章的人,可以專注于分析思路,但是不希望大家拿著POC到處搞站,本文不提供任何黑客工具,所有的POC,都是已經(jīng)公布過,無數(shù)人都知道的。
xwork修補(bǔ)漏洞的悲劇(漏洞歷史):
xwork作為struts2和webwork的核心組件,曾經(jīng)在已經(jīng)修補(bǔ)過了“xwork參數(shù)攔截器允許ognl方法執(zhí)行”漏洞,并給出了漏洞公告
http://struts.apache.org/2.x/docs/s2-003.html
S2-003
第一次修補(bǔ)
但是這個(gè)修補(bǔ),最終的結(jié)果是個(gè)悲劇。大家還記得我以前說過,在《Struts2框架安全缺陷》一文中,完全不懂web安全的開發(fā)人員,為了修補(bǔ)XSS,竟然僅僅針對(duì)POC,過濾了這段字符“〈script〉”,一旦用戶提交“〈script xxx〉”就繞過的傻X修補(bǔ)方式。
他們保持一貫的風(fēng)格,利用正則“{\\p{Graph}&&[^,#:=]}*”修補(bǔ)此漏洞。并且又僅僅針對(duì)攻擊者給出的POC,做出修補(bǔ),使用POC測(cè)試通過就發(fā)布了。
解析下xwork開發(fā)人員使用POC的幾個(gè)悲劇的測(cè)試用例。
為了讓ParametersInterceptor認(rèn)為它是個(gè)合法ognl語句,變量中必須最終包含#。
這行無法通過驗(yàn)證,java在做字符串運(yùn)算時(shí),\u0023會(huì)被轉(zhuǎn)義為#之后,才會(huì)做匹配,所以返回false。下圖可以看到,\u0023和#是完全相等的。
所以修補(bǔ)漏洞的開發(fā)人員,自作聰明,直接用正則把#干掉了。
攻擊者發(fā)來的雖然是\u0023,但是這段字符在內(nèi)存中做字符串運(yùn)算時(shí),會(huì)先變成\\u0023然后才做運(yùn)算。java在處理用戶提交的一段string包含\時(shí),為了保證數(shù)據(jù)完整性,會(huì)自動(dòng)多加一個(gè)\用做轉(zhuǎn)義,比如用戶提交了數(shù)據(jù)“\n”,在內(nèi)存中作字符串運(yùn)算時(shí),不會(huì)真的用換行做運(yùn)算,而是拿”\\n”這段字符做比較。那么用戶提交的\u0023被轉(zhuǎn)為\\u0023,就會(huì)繞過對(duì)#的檢查。
證據(jù)如圖:
這兩個(gè)case沒有通過代碼驗(yàn)證原因同上。
悲劇就在這里,這里雖然是\\u0023,符合用戶提交的場(chǎng)景,悲劇是這段字符里面因?yàn)橛锌崭瘢驮?兩邊,所以無法通過,而攻擊者如果去掉空格,就通過了。
官方就這樣發(fā)布了,其實(shí)發(fā)布的是個(gè)漏洞版。
這是第一次修補(bǔ),這次修補(bǔ)其實(shí)是大家都知道,因?yàn)榘l(fā)了公告出來,官方雖然公告說是高危,但是官方只知道攻擊者可以通過ognl表達(dá)式修改server端的session等信息。這時(shí)官方還沒有意識(shí)到這其實(shí)是個(gè)遠(yuǎn)程代碼執(zhí)行漏洞,ognl不僅僅支持修改,還支持執(zhí)行一些靜態(tài)方法,比如@Runtime@getRuntime().exec(“calc”)。 #p#
第二次修補(bǔ)
直到某天,可能是在版本xwork2.1.2時(shí),官方偷偷修改安全配置,默認(rèn)讓SecurityMemberAccess(管理ognl權(quán)限的類)的allowStaticMethodAccess為false,這導(dǎo)致靜態(tài)方法不能執(zhí)行,并且不知什么原因,偷偷修改正則,也同時(shí)放開了對(duì)參數(shù)名稱中空格字符的限制。
漏洞被爆
這次出了遠(yuǎn)程代碼執(zhí)行(Struts2/XWork < 2.2.0 Remote Command Execution Vulnerability),漏洞的發(fā)現(xiàn)者就是看到了\u0023的限制其實(shí)無效,研究出了繞過默認(rèn)安全配置的方法,并且利用ognl允許靜態(tài)方法執(zhí)行,達(dá)到了遠(yuǎn)程代碼執(zhí)行的效果。
原理簡(jiǎn)介:
1、 用戶提交了\u0023被轉(zhuǎn)義為\\u0023,通過了對(duì)參數(shù)名稱的驗(yàn)證后,最終ognl處理之前,又變成了\u0023,也就是#,符合了ognl語法。
2、 通過Ognl語句執(zhí)行,可以在struts2和webwork運(yùn)行起來時(shí),把ognl上下文中的一些默認(rèn)配置覆蓋掉,漏洞發(fā)現(xiàn)者給出了不少可以覆蓋的數(shù)值。
3、 雖然默認(rèn)配置是禁止靜態(tài)方法執(zhí)行的,但是xwork的配置,其實(shí)是可以覆蓋的,一旦覆蓋掉“用于禁止靜態(tài)方法執(zhí)行”的value,當(dāng)然又可以執(zhí)行了。
4、 Runtime.getRuntime().exec()這段,其實(shí)可以當(dāng)做靜態(tài)方法調(diào)用,導(dǎo)致執(zhí)行系統(tǒng)命令。Ognl語句調(diào)用靜態(tài)方法:@Runtime@getRuntime().exec(“calc”)。
漏洞發(fā)現(xiàn)者給出了shellcode,但是經(jīng)過我的測(cè)試,shellcode并不能影響所有版本,而是僅僅針對(duì)xwork2.1.2及以上的版本有效,xwork2.1.2及以上核心被應(yīng)用在struts2和webwork某些版本中,所以他們間接受到了影響,但是修補(bǔ)代碼,是xwork去做的。
漏洞發(fā)現(xiàn)者給出POC:
山寨的修補(bǔ)方式帶來的后果
我看到很多人對(duì)這個(gè)漏洞簡(jiǎn)單分析了下,有一小撮不明真相的群眾認(rèn)為,這是因?yàn)閰?shù)名稱\\u0023帶來的后果,官方自己上次修補(bǔ)的不完善,所以,可以過濾\\u0023,搞定這件事情。
如圖是我做見過的“其中一個(gè)”漏洞分析者,自己搞的山寨補(bǔ)丁,原理就是禁止\\u0023:
摘自互聯(lián)網(wǎng)某篇文章,不點(diǎn)名了,第二點(diǎn)解決方案居然不符合XML規(guī)則-_-!。
這只是其中一種方案,很多人說只要禁止\\u0023就可以了,不得不說,這個(gè)人應(yīng)該分析了漏洞,并且了解了原理。他知道雖然官方的補(bǔ)丁出來了,但是官方用的是白名單形式,對(duì)參數(shù)名稱限制太嚴(yán)厲,很多特殊符號(hào)不能使用,可能會(huì)導(dǎo)致部分應(yīng)用出問題,這是典型的開發(fā)人員思維。
這是官方的補(bǔ)?。?/p>
為了優(yōu)先保證業(yè)務(wù),只要禁止了\\u0023就可以修補(bǔ)漏洞,所以以上山寨方案貌似是可行的,并且經(jīng)過測(cè)試POC打不了了。
我也看到了這個(gè)方法,這真是個(gè)悲劇,攔截\u0023能解決問題么?
為什么漏洞發(fā)現(xiàn)者,通過\u0023繞過了#限制?還有沒有其他編碼可以繞過?
答案是,還有其他編碼可以繞過,僅僅控制了\u0023,是個(gè)悲劇。經(jīng)過我實(shí)際測(cè)試,發(fā)現(xiàn)#號(hào)的8進(jìn)制編碼\43,也是在這里使用的,并且\043也是可以的。于是我笑了:
這段新的POC,沒有任何一個(gè)\u0023,卻一樣可以執(zhí)行calc,經(jīng)過實(shí)際測(cè)試,繞過了所有僅僅過濾\u0023的防御。讓他們慢慢修補(bǔ)吧,我們不著急,等大家都打上了過濾\u0023的補(bǔ)丁,再把這個(gè)新的POC放出來。
我看到neeao就很謹(jǐn)慎,直接上官方補(bǔ)丁,這是他對(duì)這個(gè)漏洞的分析:
http://neeao.com/archives/59/#p#
老版本的struts和webwork的POC不通用問題
在推行修補(bǔ)方案時(shí),開發(fā)人員總是從自己的角度和經(jīng)驗(yàn)修補(bǔ)漏洞,他們采用過濾\u0023的方案也罷了,最起碼態(tài)度端正。不像有些互聯(lián)網(wǎng)公司的開發(fā)根本不去補(bǔ),原因很簡(jiǎn)單,他用的是老版本的struts和webwork。對(duì)xwork2.1.2以下核心的struts2和webwork,POC打下去沒有任何效果,所以認(rèn)為這個(gè)安全級(jí)別不高,還是等等官方公告吧(現(xiàn)在為止,官方?jīng)]有發(fā)布任何公告,修補(bǔ)好代碼提交SVN,也沒有編譯后發(fā)布版本)。
出于好奇,決定仔細(xì)研究下。我之前也不熟悉xwork源碼和ognl,以前僅僅研究過struts2的部分源碼,盲目的debug了好幾天,解決了N個(gè)問題,才搞定:
空格問題:
原POC中,會(huì)傳三個(gè)參數(shù),它們的作用,首先解析下。
1、第一個(gè)參數(shù)
在高版本的struts中,allowStaticMethodAccess(允許靜態(tài)方法訪問訪問,做權(quán)限判斷)默認(rèn)是false的。但是低版本的本來就是true,所以傳不傳都一樣,可以省了。
2、第二個(gè)參數(shù)
這句必須在,否則也不會(huì)調(diào)用靜態(tài)方法。
3、第三個(gè)參數(shù)
這句是shellcode,不能沒有。
在老版本中,第二個(gè)參數(shù),是不能運(yùn)行的,把它弄的好看點(diǎn):
注意,new后面,有空格,在ParametersInterceptor的參數(shù)正則驗(yàn)證中,根本過不去。 既然老版本不允許參數(shù)中出現(xiàn)空格,那么如果你的shell里如果有空格,會(huì)通過么?嘿嘿。。。只要你的shell,無法通過這段驗(yàn)證,就不會(huì)執(zhí)行:
以上shellcode必須對(duì)空格和:符號(hào),做16進(jìn)制的轉(zhuǎn)義,才能執(zhí)行。
PS:大家都悄悄的,不要告訴那些“只拿poc,不看技術(shù)文章的那些不明真相群眾”。
所以,要必須先解決的第一個(gè)問題是,改這個(gè)空格為\u0020,才能進(jìn)入ognl表達(dá)式的流程。
在比較新一點(diǎn)的版本的xwork中,允許空格,當(dāng)然,也是允許\u0020的,所以\u0020替換空格,就通殺了第一個(gè)問題的新老版本struts正則驗(yàn)證。
denyMethodExecution不能修改的問題:
改完之后,發(fā)現(xiàn)竟然還是不能賦值,經(jīng)過調(diào)試,在內(nèi)存中,看到的xwork.MethodAccessor.denyMethodExecution還是true,這說明這個(gè)表達(dá)式,沒有執(zhí)行成功。原因是這里做了new對(duì)象操作。先定義了#foo變量為new java.lang.Boolean類型,默認(rèn)為false,之后denyMethodExecution等于#foo。這是不允許的,原來的POC導(dǎo)致空指針異常(原因后面說),后來解決了。
總之用這個(gè),可以通殺新老版本,也不會(huì)爆空指針:
提交后再次查看內(nèi)存中的context,發(fā)現(xiàn)這個(gè)值被修改為false。
看看shellcode
Shellcode的原理是,利用ognl支持靜態(tài)方法執(zhí)行,調(diào)用java的執(zhí)行系統(tǒng)命令方法(其實(shí)完全可以調(diào)用任何java代碼,比如寫個(gè)文件等)。
Shellcode是可以做new操作的。
denyMethodExecution不能new操作,是因?yàn)檫@句執(zhí)行時(shí)的上下文中, denyMethodExecution還是true,執(zhí)行了這句,才是false,這時(shí)才可以new對(duì)象。
所以shellcode的上下文,是可以做new對(duì)象操作的。 #p#
Xwork的bug問題:
解決了這兩個(gè)問題,其實(shí)已經(jīng)給shellcode創(chuàng)造了完備的環(huán)境,按照xwork的邏輯,應(yīng)該直接讓我們調(diào)用靜態(tài)方法才是,但是在shellcode運(yùn)行時(shí),居然爆出了空指針,這個(gè)問題我研究了好久才搞明白,原來是xwork自己出了bug,到了新版本時(shí),才修補(bǔ)。
翻翻svn,看到在xwork2.1.2時(shí),偷偷修改了一段代碼。
SecurityMemberAccess這個(gè)類原來有個(gè)這樣的方法
到了xwork2.1.2時(shí),改為了:
注意標(biāo)紅的,如果name==null,就返回true。
為什么會(huì)有這行代碼呢?
它調(diào)用isExcluded(name),進(jìn)入isExcluded方法后,做正則表達(dá)式的驗(yàn)證。
如果傳進(jìn)來的是個(gè)paramName是null,并且excludeProperties是有值的,必然報(bào)錯(cuò)。Xwork的bug就是,所有的版本,調(diào)用靜態(tài)方法時(shí),都必然會(huì)傳進(jìn)來一個(gè)null,并且excludeProperties也是默認(rèn)有值的。
任何一個(gè)靜態(tài)方法的調(diào)用,這里傳進(jìn)來的都是null。
也就是說,要調(diào)用靜態(tài)方法,就必須讓excludeProperties這個(gè)家伙的值,是一個(gè)空的Set對(duì)象,否則對(duì)null對(duì)正則匹配,就會(huì)報(bào)錯(cuò)。
excludeProperties是個(gè)Set,在shell執(zhí)行的上下文中,它的值是這樣來的:
xwork處理用戶發(fā)進(jìn)來的ognl表達(dá)式時(shí),會(huì)用xwork的SecurityMemberAccess做權(quán)限判斷,以保證靜態(tài)方法不會(huì)被人隨便調(diào)用。原理是傳給shell執(zhí)行的上下文中幾個(gè)默認(rèn)配置,其中一個(gè)是默認(rèn)的allowStaticMethodAccess=true,也會(huì)給excludeProperties這個(gè)Set對(duì)象會(huì)被添加一個(gè)value,結(jié)果就不是空的Set對(duì)象了。
所以,在shellcode上下文中,執(zhí)行靜態(tài)方法必然會(huì)出錯(cuò)。
讓shellcode正常執(zhí)行,解決方法的思路就是new一個(gè)新的HashSet。
但是經(jīng)過我測(cè)試,這里還是不能做new的操作,因?yàn)閚ew的時(shí)候,會(huì)調(diào)用構(gòu)造方法,這實(shí)質(zhì)上還是在調(diào)用方法,調(diào)用方法就會(huì)出錯(cuò)。為了證明我的猜測(cè),debug調(diào)用到這一步的時(shí)候,手工修改它為new HashSet(),立刻就通過了。不能new,又怎么能得到一個(gè)空的Set呢?這是最后一道關(guān)卡。為了搞定它,我甚至查了struts-default.xml在內(nèi)存中的位置,考慮是不是使用ognl修改掉。
因?yàn)檫@個(gè)放棄了一段時(shí)間,很郁悶,當(dāng)我再次拾起來,卻突然看到了這個(gè)值的默認(rèn)值。
跟進(jìn)emptySet();
這是個(gè)有意思的知識(shí)點(diǎn),我也是第一次了解到,可以使用EMPTY_SET,取到一個(gè)空的set。
于是當(dāng)我提交某些東西,讓
這一句覆蓋了默認(rèn)的那個(gè)有了一個(gè)值的map,重新變成了空的map,這才真正的達(dá)到了靜態(tài)方法的終極調(diào)用條件。
其實(shí)我都快吐血了,明明是xwork自己代碼寫的不嚴(yán)謹(jǐn),直到他們新的版本才發(fā)現(xiàn)這個(gè)bug,并修補(bǔ),居然導(dǎo)致我研究漏洞的同時(shí)還順帶幫他們處理bug。
最后終于找到機(jī)會(huì)鄙視一下漏洞發(fā)現(xiàn)者的poc,他說老版本的struts2可以使用XXX的POC,經(jīng)過我測(cè)試,不可行,原理也很簡(jiǎn)單,本文說了,POC中不能有空格。
這句話即使在新版本中,也是沒什么影響的,雖然會(huì)改變一個(gè)值,但是不受啥影響, shellcode中,有了這句話,就兼容新老版本,一切通殺了!
最后湊出通殺0DAY在這里:
被和諧了,沒辦法,畢竟在互聯(lián)網(wǎng)公司里,要考慮到其他互聯(lián)網(wǎng)公司同行的處境,雖然他們不一定都是漏洞公布的POC不能打的版本。
POC,不要找我要了,本文僅僅說下技術(shù)研究。
回顧下:
適合新版本繞過雙引號(hào)和\u0023防御的poc:
適合新版本繞過\u0023的poc
僅僅適用老版本(struts2.0.12)的POC
僅僅適用新版本的也就是漏洞發(fā)現(xiàn)者放出來的poc
【編輯推薦】