一、Java SpEL表達式基本原理
SpEL(Spring Expression Language)簡稱Spring表達式語言,在Spring 3中引入。
SpEL能在運行時構建複雜表達式、存取對象圖屬性、對象方法調用等等,可以與基於XML和基於註解的Spring配置還有bean定義一起使用。
在Spring系列產品中,SpEL是表達式計算的基礎,實現了與Spring生態系統所有產品無縫對接。Spring框架的核心功能之一就是通過依賴注入的方式來管理Bean之間的依賴關係,而SpEL可以方便快捷的對ApplicationContext中的Bean進行屬性的裝配和提取。由於它能夠在運行時動態分配值,因此可以爲我們節省大量Java代碼。
SpEL有許多特性:
- 使用Bean的ID來引用Bean
- 可調用方法和訪問對象的屬性
- 可對值進行算數、關係和邏輯運算
- 可使用正則表達式進行匹配
- 可進行集合操作
SpEL是單獨模塊,只依賴於core模塊,不依賴於其他模塊,可以單獨使用。
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>5.1.9.RELEASE</version> </dependency>
0x1:SpEL定界符
SpEL使用#{}作爲定界符,所有在大括號中的字符都將被認爲是SpEL表達式,在其中可以使用SpEL運算符、變量、引用bean及其屬性和方法等。
這裏需要注意#{}和${}的區別:
- #{}就是SpEL的定界符,用於指明內容爲SpEL表達式並執行
- ${}主要用於加載外部屬性文件中的值
- 兩者可以混合使用,但是必須#{}在外面,${}在裏面,如#{'${}'},注意單引號是字符串類型才添加的
0x2:SpEL用法
SpEL常見的三種用法:
- 在@Value註解中使用
- 在XML配置中使用
- 在代碼中創建Expression對象,利用Expression對象來執行SpEL
1、在@Value註解中使用
@Configuration("testConfig11") public class TestConfig { @Bean(name = "user11") public User user11() { User user = new User(); user.setId("123"); return user; } } @RestController public class TestController { @Value("#{user11.id}") private String userId; }
2、在XML配置中使用
1)示例一、字面值
最簡單的SpEL表達式就是僅包含一個字面值,下面我們在XML配置文件中使用SpEL設置類屬性的值爲字面值,此時需要用到#{}
定界符,注意若是指定爲字符串的話需要添加單引號括起來。
直接用Spring的HelloWorld例子。
package com.example.spring_helloworld; public class HelloWorld { private String message; public void setMessage(String message){ this.message = message; } public void getMessage(){ System.out.println("Your Message : " + message); } }
package com.example.spring_helloworld; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; @SpringBootApplication public class SpringHelloworldApplication { public static void main(String[] args) { // SpringApplication.run(SpringHelloworldApplication.class, args); ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml"); HelloWorld obj = (HelloWorld) context.getBean("helloWorld"); obj.getMessage(); } }
Beans.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd "> <bean id="helloWorld" class="com.example.spring_helloworld.HelloWorld"> <property name="message" value="#{'littlehann'} is #{666}" /> </bean> </beans>
2)示例二、引用Bean、屬性和方法
SpEL表達式能夠通過其他Bean的ID進行引用,直接在#{}
符號中寫入ID名即可,無需添加單引號括起來。如:
<!--原來的寫法,通過構造函數實現依賴注入--> <!--<constructor-arg ref="test"/>--> <constructor-arg value="#{test}"/>
SpEL表達式也能夠訪問類的屬性,比如,carl參賽者是一位模仿高手,kenny唱什麼歌,彈奏什麼樂器,他就唱什麼歌,彈奏什麼樂器:
<bean id="kenny" class="com.spring.entity.Instrumentalist" p:song="May Rain" p:instrument-ref="piano"/> <bean id="carl" class="com.spring.entity.Instrumentalist"> <property name="instrument" value="#{kenny.instrument}"/> <property name="song" value="#{kenny.song}"/> </bean>
key指定kenny<bean> 的id,value指定kenny<bean>的song屬性。其等價於執行下面的代碼:
Instrumentalist carl = new Instrumentalist(); carl.setSong(kenny.getSong());
3)示例三、引用類方法
SpEL表達式還可以訪問類的方法。
假設現在有個SongSelector類,該類有個selectSong()方法,這樣的話carl就可以不用模仿別人,開始唱songSelector所選的歌了:
<property name="song" value="#{SongSelector.selectSong()}"/>
carl有個癖好,歌曲名不是大寫的他就渾身難受,我們現在要做的就是僅僅對返回的歌曲調用toUpperCase()
方法:
<property name="song" value="#{SongSelector.selectSong().toUpperCase()}"/>
3、在代碼中創建Expression對象,利用Expression對象來執行SpEL
- 首先構造一個解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默認實現。
- 其次解析器解析字符串表達式:使用 ExpressionParser 的 parseExpression 來解析相應的表達式爲 Expression 對象。
- 再次構造上下文:準備比如變量定義等等表達式需要的上下文數據。
- 最後根據上下文得到表達式運算後的值:通過 Expression 接口的 getValue 方法根據上下文獲得表達式值。
package com.example.spring_helloworld; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; @SpringBootApplication public class SpringHelloworldApplication { public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("('Hello' + ' Littlehann').concat(#end)"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("end", "!"); System.out.println(expression.getValue(context)); } }
涉及到的主要接口如下,
- ExpressionParser 接口:表示解析器,默認實現是 org.springframework.expression.spel.standard 包中的 SpelExpressionParser 類,使用 parseExpression 方法將字符串表達式轉換爲 Expression 對象,對於 ParserContext 接口用於定義字符串表達式是不是模板,及模板開始與結束字符;
- EvaluationContext 接口:表示上下文環境,默認實現是 org.springframework.expression.spel.support 包中的 StandardEvaluationContext 類,使用 setRootObject 方法來設置根對象,使用 setVariable 方法來註冊自定義變量,使用 registerFunction 來註冊自定義函數等等。
- Expression 接口:表示表達式對象,默認實現是 org.springframework.expression.spel.standard 包中的 SpelExpression,提供 getValue 方法用於獲取表達式值,提供 setValue 方法用於設置對象值。
二、SpEL命令執行漏洞原理分析
表達式語言主要是解析表達式爲AST語法樹計算每個樹節點,當用戶可以控制輸入的表達式時,並且繞過黑名單限制則可達到RCE。
在XML中配置使用SpEL只要修改Beans.xml中value中類類型表達式的類爲Runtime並調用其命令執行方法即可:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd "> <bean id="helloWorld" class="com.example.spring_helloworld.HelloWorld"> <!--<property name="message" value="#{'littlehann'} is #{666}" />--> <property name="message" value="#{T(java.lang.Runtime).getRuntime().exec('open -a Calculator')}" /> </bean> </beans>
但是大多數實戰環境下,很多種Spring CVE漏洞都是基於Expression形式的SpEL表達式注入。
和前面XML配置的用法區別在於程序會將這裏傳入parseExpression()函數的字符串參數當初SpEL表達式來解析,而無需通過#{}
符號來註明:
package com.example.spring_helloworld; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; @SpringBootApplication public class SpringHelloworldApplication { public static void main(String[] args) { // 操作類彈計算器,當然java.lang包下的類是可以省略包名的 String spel = "T(java.lang.Runtime).getRuntime().exec(\"open -a Calculator\")"; // String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")"; ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(spel); EvaluationContext context = new StandardEvaluationContext(); System.out.println(expression.getValue(context)); } }
org.springframework.expression.spel.standard.SpelExpression.getValue()首先會解析生成三個AST節點,
- java.lang.Runtime的TypeReference
- 2個MethodReference分別是getRuntime和exec
通過SpelNodeImpl.getValue()調用CompoundExpression.getValueInternal()處理,首先通過getValueRef獲取ref,再調用ref.getValue計算最後的結果。
跟進getValueRef()看下,循環計算除前n-1個node的結果,然後調用nextNode.getValueRef(state)獲取最終的ref。
這裏nextNode就是MethodReference,調用MethodReference.getValueRef()返回MethodReference$MethodValueRef實例。
跟進ref.getValue會調用getValueInternal,getValueInternal調用ReflectiveMethodExecutor.execute()通過執行方法。
ReflectiveMethodExecutor.execute()通過反射執行方法調用method.invoke。
參考鏈接:
https://www.mi1k7ea.com/2020/01/10/SpEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/ https://blog.51cto.com/u_14120/6802047 https://r17a-17.github.io/2021/11/22/Java%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/#SpEL
三、檢測與防禦方法
0x1:檢測方法
全局搜索關鍵特徵:
//關鍵類 org.springframework.expression.Expression org.springframework.expression.ExpressionParser org.springframework.expression.spel.standard.SpelExpressionParser //調用特徵 ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(str); expression.getValue() expression.setValue()
0x2:防禦方法
最直接的修復方法是使用SimpleEvaluationContext替換StandardEvaluationContext。
package com.example.spring_helloworld; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; @SpringBootApplication public class SpringHelloworldApplication { public static void main(String[] args) { String spel = "T(java.lang.Runtime).getRuntime().exec(\"open -a Calculator\")"; ExpressionParser parser = new SpelExpressionParser(); HelloWorld student = new HelloWorld(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(student).build(); Expression expression = parser.parseExpression(spel); System.out.println(expression.getValue(context)); } }