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