Java代碼審計之SpEL表達式注入
一、SpEL 表達式注入
Spring Expression Language(簡稱 SpEL)是一種功能強大的表達式語言、用于在運行時查詢和操作對象圖;語法上類似于 Unified EL,但提供了更多的特性,特別是方法調(diào)用和基本字符串模板函數(shù)。SpEL 的誕生是為了給 Spring 社區(qū)提供一種能夠與 Spring 生態(tài)系統(tǒng)所有產(chǎn)品無縫對接,能提供一站式支持的表達式語言。
二、SpEL 表達式
- 基本表達式:字面量表達式、關(guān)系,邏輯與算數(shù)運算表達式、字符串鏈接及截取表達式、三目運算、正則表達式以及括號優(yōu)先級表達式;
- 類相關(guān)表達式:類類型表達式、類實例化、instanceof 表達式、變量定義及引用、賦值表達式、自定義函數(shù)、對象屬性存取及安全導(dǎo)航表達式、對象方法調(diào)用、Bean 引用;
- 集合相關(guān)表達式:內(nèi)聯(lián) List、內(nèi)聯(lián)數(shù)組、集合、字典訪問、列表、字典;
- 其他表達式:模版表達式
三、SpEL 基礎(chǔ)
在 pom.xml 導(dǎo)入 maven 或是把”org.springframework.expression-3.0.5.RELEASE.jar”添加到類路徑中
- <properties>
- <org.springframework.version>5.0.8.RELEASE</org.springframework.version>
- </properties>
- <dependency>
- <groupId>org.springframework</groupId>
- <artifactId>spring-expression</artifactId>
- <version>${org.springframework.version}</version>
- </dependency>
1. SpEL 使用方式
SpEL 在求表達式值時一般分為四步,其中第三步可選:首先構(gòu)造一個解析器,其次解析器解析字符串表達式,在此構(gòu)造上下文,然后根據(jù)上下文得到表達式運算后的值。
- ExpressionParser parser = new SpelExpressionParser();
- Expression expression = parser.parseExpression("('Hello' + ' freebuf').concat(#end)");
- EvaluationContext context = new StandardEvaluationContext();
- context.setVariable("end", "!");
- System.out.println(expression.getValue(context));
- 創(chuàng)建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默認實現(xiàn);
- 解析表達式:使用 ExpressionParser 的 parseExpression 來解析相應(yīng)的表達式為 Expression 對象。
- 構(gòu)造上下文:準備比如變量定義等等表達式需要的上下文數(shù)據(jù)。
- 求值:通過 Expression 接口的 getValue 方法根據(jù)上下文獲得表達式值。
2. SpEL 主要接口
1.ExpressionParser 接口:表示解析器,默認實現(xiàn)是 org.springframework.expression.spel.standard 包中的 SpelExpressionParser 類,使用 parseExpression 方法將字符串表達式轉(zhuǎn)換為 Expression 對象,對于 ParserContext 接口用于定義字符串表達式是不是模板,及模板開始與結(jié)束字符;
- public interface ExpressionParser {
- Expression parseExpression(String expressionString);
- Expression parseExpression(String expressionString, ParserContext context);
- }
事例 demo:
- ExpressionParser parser = new SpelExpressionParser();
- ParserContext parserContext = new ParserContext() {
- @Override
- public boolean isTemplate() {
- return true;
- }
- @Override
- public String getExpressionPrefix() {
- return "#{";
- }
- @Override
- public String getExpressionSuffix() {
- return "}";
- }
- };
- String template = "#{'hello '}#{'freebuf!'}";
- Expression expression = parser.parseExpression(template, parserContext);
- System.out.println(expression.getValue());
演示的是使用 ParserContext 的情況,此處定義了 ParserContext 實現(xiàn):定義表達式是模塊,表達式前綴為「#{」,后綴為「}」;使用 parseExpression 解析時傳入的模板必須以「#{」開頭,以「}」結(jié)尾。
默認傳入的字符串表達式不是模板形式,如之前演示的 Hello World。
- EvaluationContext 接口:表示上下文環(huán)境,默認實現(xiàn)是 org.springframework.expression.spel.support 包中的 StandardEvaluationContext 類,使用 setRootObject 方法來設(shè)置根對象,使用 setVariable 方法來注冊自定義變量,使用 registerFunction 來注冊自定義函數(shù)等等。
- Expression 接口:表示表達式對象,默認實現(xiàn)是 org.springframework.expression.spel.standard 包中的 SpelExpression,提供 getValue 方法用于獲取表達式值,提供 setValue 方法用于設(shè)置對象值。
3. SpEL 語法 – 類相關(guān)表達式
類類型表達式:使用”T(Type)”來表示 java.lang.Class 實例,”Type”必須是類全限定名,”java.lang”包除外,即該包下的類可以不指定包名;使用類類型表達式還可以進行訪問類靜態(tài)方法及類靜態(tài)字段。
具體使用方法:
- ExpressionParser parser = new SpelExpressionParser();
- // java.lang 包類訪問
- Class<String> result1 = parser.parseExpression("T(String)").getValue(Class.class);
- System.out.println(result1);
- //其他包類訪問
- String expression2 = "T(java.lang.Runtime).getRuntime().exec('open /Applications/Calculator.app')";
- Class<Object> result2 = parser.parseExpression(expression2).getValue(Class.class);
- System.out.println(result2);
- //類靜態(tài)字段訪問
- int result3 = parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class);
- System.out.println(result3);
- //類靜態(tài)方法調(diào)用
- int result4 = parser.parseExpression("T(Integer).parseInt('1')").getValue(int.class);
- System.out.println(result4);
- 類實例化:類實例化同樣使用 java 關(guān)鍵字「new」,類名必須是全限定名,但 java.lang 包內(nèi)的類型除外,如 String、Integer。
- instanceof 表達式:SpEL 支持 instanceof 運算符,跟 Java 內(nèi)使用同義;如”‘haha’ instanceof T(String)”將返回 true。
- 變量定義以及引用:變量定義通過 EvaluationContext 接口的 setVariable(variableName, value) 方法定義;在表達式中使用”#variableName”引用;除了引用自定義變量,SpE 還允許引用根對象及當前上下文對象,使用”#root”引用根對象,使用”#this”引用當前上下文對象;
- 自定義函數(shù):目前只支持類靜態(tài)方法注冊為自定義函數(shù);SpEL 使用 StandardEvaluationContext 的 registerFunction 方法進行注冊自定義函數(shù),其實完全可以使用 setVariable 代替,兩者其實本質(zhì)是一樣的
四、審計過程
這里拿 Spring Message 遠程命令執(zhí)行漏洞來作為例子
1. 環(huán)境搭建
- git clone https://github.com/spring-guides/gs-messaging-stomp-websocket
- git checkout 6958af0b02bf05282673826b73cd7a85e84c12d3
拿到項目代碼,全局搜索一下 org.springframework.expression.spel.standard,發(fā)現(xiàn) DefaultSubscriptionRegistry.java 文件處有導(dǎo)入。
再搜索一下 SpelExpressionParser
往下跟進發(fā)現(xiàn)如下關(guān)鍵代碼,具體分析看代碼注釋
- @Override
- protected void addSubscriptionInternal(
- String sessionId, String subsId, String destination, Message<?> message) {
- Expression expression = null;
- MessageHeaders headers = message.getHeaders();
- // 這里可以看出 SpEL 表達式 expression 是從 headers 中的 selector 字段中取出來
- String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
- if (selector != null) {
- try {
- //生成 expression 對象
- expression = this.expressionParser.parseExpression(selector);
- this.selectorHeaderInUse = true;
- if (logger.isTraceEnabled()) {
- logger.trace("Subscription selector: [" + selector + "]");
- }
- }
- catch (Throwable ex) {
- if (logger.isDebugEnabled()) {
- logger.debug("Failed to parse selector: " + selector, ex);
- }
- }
- }
- // expression 傳入 addSubscription 這個函數(shù)里面,即存放在 this.subscriptionRegistry
- this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
- this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
- }
再搜索一下 this.subscriptionRegistry,看看有沒有調(diào)用傳進去的 expression。
然后發(fā)現(xiàn)了!
在這里調(diào)用了 this.subscriptionRegistry.getSubscriptions(sessionId) 并從中取出 info->sub-> expression。
最關(guān)鍵的是,這里直接調(diào)用了 expression.getValue()!這說明如果能控制 SpEL 的表達式,就能直接命令執(zhí)行!
再來看看這個 filterSubscriptions 函數(shù)在哪里調(diào)用。從函數(shù)的調(diào)用回溯追蹤調(diào)用鏈如下:
- filterSubscriptions -> findSubscriptionsInternal -> findSubscriptions -> sendMessageToSubscribers
2. sendMessageToSubscribers 即發(fā)送消息的功能
回顧一下整個流程,SpEL 表達式從 headers 中 selector 獲取,即發(fā)送請求時添加 selector 到請求的 header 即可傳入,然后生成 expression 對象傳入 this.subscriptionRegistry,然后當發(fā)送消息的時候,最終會直接從 this.subscriptionRegistry 取出并調(diào)用 expression.getValue() 執(zhí)行我們傳入的 SpEL 表達式。
驗證過程,在 expression.getValue() 這里打個斷點,看看發(fā)送消息是否會攔截并查看調(diào)用鏈是否如上述分析一樣。
Bingo!
簡單總結(jié)一下 SpEL 表達式注入的分析思路,可以先全局搜索 org.springframework.expression.spel.standard, 或是 expression.getValue()、expression.setValue(),定位到具體漏洞代碼,再分析傳入的參數(shù)能不能利用,然后再追蹤參數(shù)來源,看看是否可控。Spring Data Commons Remote Code Execution 的 SpEL 注入導(dǎo)致的代碼執(zhí)行同樣可以用類似的思路分析。
五、漏洞修復(fù)
SimpleEvaluationContext、StandardEvaluationContext 是 SpEL 提供的兩個 EvaluationContext:
- SimpleEvaluationContext - 針對不需要 SpEL 語言語法的全部范圍并且應(yīng)該受到有意限制的表達式類別,公開 Spal 語言特性和配置選項的子集。
- StandardEvaluationContext - 公開全套 SpEL 語言功能和配置選項。您可以使用它來指定默認的根對象并配置每個可用的評估相關(guān)策略。
SimpleEvaluationContext 旨在僅支持 SpEL 語言語法的一個子集。它不包括 Java 類型引用,構(gòu)造函數(shù)和 bean 引用;所以最直接的修復(fù)方式是使用 SimpleEvaluationContext 替換 StandardEvaluationContext。
這是我個人學(xué)習(xí)代碼審計過程中的小總結(jié),可能邏輯性相對來說沒那么嚴謹,但是個人覺得這是一個比較通俗易懂的分析方法,不喜勿噴。