影響版本:
1.1.0-1.1.12
1.2.0-1.2.7
1.3.0
修復方案:升至1.3.1或以上版本
我的測試環境:SpringBoot 1.2.0
0x00前言
這是2016年爆出的一個洞,利用條件是使用了springboot的默認錯誤頁(Whitelabel Error Page),存在漏洞的頁面在:/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.java
0x01觸發原因
本次漏洞的觸發點在SpringBoot的自定義錯誤頁面,功能是頁面返回錯誤,並提供詳細信息,信息中包括錯誤status("status"->500)、時間戳("timestamp"->"Fri Dec.....")、錯誤信息("error"->"Internal Server Error")、和用戶輸入的參數("message"->"abcd"),這些參數在模板文件中以類似於以下形式存在:”Error 1234 ${status}---${timestamp}---${error}---${message}“。
后端進行渲染視圖時,首先,解析錯誤頁面模板中的參數名(status、timestamp、error、message),即判斷模板中每個${的位置,然后再判斷最近的}的位置,從而將參數名一個個讀取出來,然而這里使用了遞歸,也就是說如果參數名中還包含${和}的話,這個解析引擎會再次遞歸一次,再次解析這個值,如,模板中有個值為${${abc}},由於使用了遞歸,解析引擎會對其解析兩次,第一層去掉最外層的{}解析成${abc},然后將其作為參數進行第二次解析。在第二次解析中將里層的{}去掉,變成abc。
每次將一個參數名解析出來之后,就將參數名傳入SpEL引擎,解析成context中對應參數名的值(如"status"->500),完成之后返回參數值給第一步中的解析引擎(返回500)。
解析引擎收到SpEL傳回的參數值之后,再次進行遞歸,以防參數值中也存在${和},存在則去之,然后在遞歸過程中再次傳入SpEL引擎進行解析。這里就是觸發點了。假設用戶的輸入中包含${payload},則SpEL第一次message解析成${payload}之后,解析引擎進行遞歸,去掉${和}后將payload傳入SpEL引擎,SpEL引擎將將直接對payload進行解析,從而觸發了漏洞,觸發點如下圖所示。
0x02調試分析
先搭好存在漏洞的SpringBoot版本的環境,使用其自帶的sample搭建一個服務器,然后自己寫一個控制器,拋出異常即可。
開啟調試,使用瀏覽器訪問
http://127.0.0.1:8080/?payload=${new%20java.lang.ProcessBuilder(new%20java.lang.String(new%20byte[]{99,97,108,99})).start()}
首先,將context賦值到this.context中,然后以this.template和this.resolver為參數調用replacePlaceholders方法。
this.template="<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>${timestamp}</div><div>There was an unexpected error (type=${error}, status=${status}).</div><div>${message}</div></body></html>"
跟進replacePlaceholders方法,進入了PropertyPlaceholderHelper文件
繼續跟進parseStringValue方法(這就是分析中說的存在遞歸的方法),strVal的值為之前的this.template,將之賦值給result,然后通過判斷result中${和}的位置,開始解析result中的第一個參數名,並賦值給placeholder,本次的值為"timestamp",然后將placeholder作為第一個參數,再次調用本方法(遞歸,以防字符串placeholder中包含${})。
跟進遞歸,由於placeholder的值為"timestamp",其中不包含${,導致startIndex為-1,故不進入while語句,直接到了return,因此可以發現,在遞歸時,如果第一個參數中不包含${,則直接將第一個參數返回。
再次回到之前的點,下一步是調用resolvePlaceholder方法,此函數的作用是查找this.context中對應參數的值並返回,跟進看一下
首先看一下this.context,發現"timestamp" -> "Sat Dec 15 10:49:02 CST 2018"
繼續跟進,發現value被賦值成SpEL解析后的值,然后return
回到parseStringValue方法,將經過SpEL解析后return的值賦值給propVal,由於propVal != null,故跳過第一個if語句,進入第二個語句,將propVal作為第一個參數再次遞歸。通過上一次遞歸我們發現,如果第一個參數中沒有${,則直接返回第一個參數的值,因此這次就不再跟進了。
遞歸回來后propVal的值沒變,使用replace將propVal替換到result中的對應的參數位。接着尋找template中的下一個參數位,賦值給startIndex,用於下一次while條件判斷。
進入第二次循環,這次的參數是error,和之前的timestamp過程一樣,就不具體分析了
第三次while循環,參數是status,同上,不具體分析
進入第四次循環,重頭戲來啦,這次的參數是message,其值是用戶輸入的值。
跟進到第一次遞歸,防止參數名中含有${},由於參數名士message,故略過這一步。然后到了resolvePlaceholder方法,用於使用SpEL表達式引擎解析message的值,再跟進一下。
發現value的值為用戶傳入的payload
其中包含${},是一個SpEL表達式。繼續跟進,返回到parseStringValue方法。
可以發現,為了防止propVal中包含${},再次進行一次遞歸。下面就是漏洞關鍵點了,跟進這次遞歸。
此時placeholder的值為去掉${}的payload,即:"new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()"。將placeholder作為第一個參數傳入SpEL解析函數(147行)
可以發現,這里直接使用parseExpression(name),而name的值就是我們的payload。接着使用getValue解析payload:
Expression expression = this.parser.parseExpression("new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()");
Object value = expression.getValue(this.context);
然后觸發payload,漏洞利用完成。
0x03補丁分析
補丁創建了一個新的NonRecursivePropertyPlaceholderHelper類,用於防止parseStringValue進行遞歸解析。