Java SPEL表達(dá)式注入漏洞原理解析
Java SPEL表達(dá)式注入漏洞原理研究
一、Java SpEL表達(dá)式基本原理
SpEL(Spring Expression Language)簡(jiǎn)稱Spring表達(dá)式語(yǔ)言,在Spring 3中引入。
SpEL能在運(yùn)行時(shí)構(gòu)建復(fù)雜表達(dá)式、存取對(duì)象圖屬性、對(duì)象方法調(diào)用等等,可以與基于XML和基于注解的Spring配置還有bean定義一起使用。
在Spring系列產(chǎn)品中,SpEL是表達(dá)式計(jì)算的基礎(chǔ),實(shí)現(xiàn)了與Spring生態(tài)系統(tǒng)所有產(chǎn)品無(wú)縫對(duì)接。Spring框架的核心功能之一就是通過(guò)依賴注入的方式來(lái)管理Bean之間的依賴關(guān)系,而SpEL可以方便快捷的對(duì)ApplicationContext中的Bean進(jìn)行屬性的裝配和提取。由于它能夠在運(yùn)行時(shí)動(dòng)態(tài)分配值,因此可以為我們節(jié)省大量Java代碼。
SpEL有許多特性:
- 使用Bean的ID來(lái)引用Bean
- 可調(diào)用方法和訪問(wèn)對(duì)象的屬性
- 可對(duì)值進(jìn)行算數(shù)、關(guān)系和邏輯運(yùn)算
- 可使用正則表達(dá)式進(jìn)行匹配
- 可進(jìn)行集合操作
SpEL是單獨(dú)模塊,只依賴于core模塊,不依賴于其他模塊,可以單獨(dú)使用。
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>5.1.9.RELEASE</version> </dependency>
0x1:SpEL定界符
SpEL使用#{}作為定界符,所有在大括號(hào)中的字符都將被認(rèn)為是SpEL表達(dá)式,在其中可以使用SpEL運(yùn)算符、變量、引用bean及其屬性和方法等。
這里需要注意#{}和${}的區(qū)別:
- #{}就是SpEL的定界符,用于指明內(nèi)容為SpEL表達(dá)式并執(zhí)行
- ${}主要用于加載外部屬性文件中的值
- 兩者可以混合使用,但是必須#{}在外面,${}在里面,如#{'${}'},注意單引號(hào)是字符串類型才添加的
0x2:SpEL用法
SpEL常見(jiàn)的三種用法:
- 在@Value注解中使用
- 在XML配置中使用
- 在代碼中創(chuàng)建Expression對(duì)象,利用Expression對(duì)象來(lái)執(zhí)行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)
示例一、字面值
最簡(jiǎn)單的SpEL表達(dá)式就是僅包含一個(gè)字面值,下面我們?cè)赬ML配置文件中使用SpEL設(shè)置類屬性的值為字面值,此時(shí)需要用到#{}
定界符,注意若是指定為字符串的話需要添加單引號(hào)括起來(lái)。
直接用Spring的HelloWorld例子。
HelloWorld.java
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); } }
SpringHelloworldApplication.java
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表達(dá)式能夠通過(guò)其他Bean的ID進(jìn)行引用,直接在#{}
符號(hào)中寫入ID名即可,無(wú)需添加單引號(hào)括起來(lái)。如:
<!--原來(lái)的寫法,通過(guò)構(gòu)造函數(shù)實(shí)現(xiàn)依賴注入--> <!--<constructor-arg ref="test"/>--> <constructor-arg value="#{test}"/>
SpEL表達(dá)式也能夠訪問(wèn)類的屬性,比如,carl參賽者是一位模仿高手,kenny唱什么歌,彈奏什么樂(lè)器,他就唱什么歌,彈奏什么樂(lè)器:
<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屬性。其等價(jià)于執(zhí)行下面的代碼:
Instrumentalist carl = new Instrumentalist(); carl.setSong(kenny.getSong());
3)示例三、引用類方法
SpEL表達(dá)式還可以訪問(wèn)類的方法。
假設(shè)現(xiàn)在有個(gè)SongSelector類,該類有個(gè)selectSong()方法,這樣的話carl就可以不用模仿別人,開始唱songSelector所選的歌了:
<property name="song" value="#{SongSelector.selectSong()}"/>
carl有個(gè)癖好,歌曲名不是大寫的他就渾身難受,我們現(xiàn)在要做的就是僅僅對(duì)返回的歌曲調(diào)用toUpperCase()
方法:
<property name="song" value="#{SongSelector.selectSong().toUpperCase()}"/>
3、在代碼中創(chuàng)建Expression對(duì)象,利用Expression對(duì)象來(lái)執(zhí)行SpEL
SpEL 在求表達(dá)式值時(shí)一般分為四步,其中第三步可選:
- 首先構(gòu)造一個(gè)解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默認(rèn)實(shí)現(xiàn)。
- 其次解析器解析字符串表達(dá)式:使用 ExpressionParser 的 parseExpression 來(lái)解析相應(yīng)的表達(dá)式為 Expression 對(duì)象。
- 再次構(gòu)造上下文:準(zhǔn)備比如變量定義等等表達(dá)式需要的上下文數(shù)據(jù)。
- 最后根據(jù)上下文得到表達(dá)式運(yùn)算后的值:通過(guò) Expression 接口的 getValue 方法根據(jù)上下文獲得表達(dá)式值。
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 接口:表示解析器,默認(rèn)實(shí)現(xiàn)是 org.springframework.expression.spel.standard 包中的 SpelExpressionParser 類,使用 parseExpression 方法將字符串表達(dá)式轉(zhuǎn)換為 Expression 對(duì)象,對(duì)于 ParserContext 接口用于定義字符串表達(dá)式是不是模板,及模板開始與結(jié)束字符;
- EvaluationContext 接口:表示上下文環(huán)境,默認(rèn)實(shí)現(xiàn)是 org.springframework.expression.spel.support 包中的 StandardEvaluationContext 類,使用 setRootObject 方法來(lái)設(shè)置根對(duì)象,使用 setVariable 方法來(lái)注冊(cè)自定義變量,使用 registerFunction 來(lái)注冊(cè)自定義函數(shù)等等。
- Expression 接口:表示表達(dá)式對(duì)象,默認(rèn)實(shí)現(xiàn)是 org.springframework.expression.spel.standard 包中的 SpelExpression,提供 getValue 方法用于獲取表達(dá)式值,提供 setValue 方法用于設(shè)置對(duì)象值。
二、SpEL命令執(zhí)行漏洞原理分析
表達(dá)式語(yǔ)言主要是解析表達(dá)式為AST語(yǔ)法樹計(jì)算每個(gè)樹節(jié)點(diǎn),當(dāng)用戶可以控制輸入的表達(dá)式時(shí),并且繞過(guò)黑名單限制則可達(dá)到RCE。
在XML中配置使用SpEL只要修改Beans.xml中value中類類型表達(dá)式的類為Runtime并調(diào)用其命令執(zhí)行方法即可:
<?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>
但是大多數(shù)實(shí)戰(zhàn)環(huán)境下,很多種Spring CVE漏洞都是基于Expression形式的SpEL表達(dá)式注入。
和前面XML配置的用法區(qū)別在于程序會(huì)將這里傳入parseExpression()函數(shù)的字符串參數(shù)當(dāng)初SpEL表達(dá)式來(lái)解析,而無(wú)需通過(guò)#{}
符號(hào)來(lái)注明:
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) { // 操作類彈計(jì)算器,當(dāng)然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()首先會(huì)解析生成三個(gè)AST節(jié)點(diǎn),
- java.lang.Runtime的TypeReference
- 2個(gè)MethodReference分別是getRuntime和exec
通過(guò)SpelNodeImpl.getValue()調(diào)用CompoundExpression.getValueInternal()處理,首先通過(guò)getValueRef獲取ref,再調(diào)用ref.getValue計(jì)算最后的結(jié)果。
跟進(jìn)getValueRef()看下,循環(huán)計(jì)算除前n-1個(gè)node的結(jié)果,然后調(diào)用nextNode.getValueRef(state)獲取最終的ref。
這里nextNode就是MethodReference,調(diào)用MethodReference.getValueRef()返回MethodReference$MethodValueRef實(shí)例。
跟進(jìn)ref.getValue會(huì)調(diào)用getValueInternal,getValueInternal調(diào)用ReflectiveMethodExecutor.execute()通過(guò)執(zhí)行方法。
ReflectiveMethodExecutor.execute()通過(guò)反射執(zhí)行方法調(diào)用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
三、檢測(cè)與防御方法
0x1:檢測(cè)方法
全局搜索關(guān)鍵特征:
//關(guān)鍵類 org.springframework.expression.Expression org.springframework.expression.ExpressionParser org.springframework.expression.spel.standard.SpelExpressionParser //調(diào)用特征 ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(str); expression.getValue() expression.setValue()
0x2:防御方法
最直接的修復(fù)方法是使用SimpleEvaluationContext替換StandardEvaluationContext。
官方文檔:https://docs.spring.io/spring/docs/5.0.6.RELEASE/javadoc-api/org/springframework/expression/spel/support/SimpleEvaluationContext.html
//關(guān)鍵類 org.springframework.expression.Expression org.springframework.expression.ExpressionParser org.springframework.expression.spel.standard.SpelExpressionParser //調(diào)用特征 ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(str); expression.getValue() expression.setValue()
到此這篇關(guān)于Java SPEL表達(dá)式注入漏洞原理研究的文章就介紹到這了,更多相關(guān)Java SPEL表達(dá)式注入漏洞內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot實(shí)現(xiàn)無(wú)限級(jí)評(píng)論回復(fù)的項(xiàng)目實(shí)踐
本文主要介紹了SpringBoot實(shí)現(xiàn)無(wú)限級(jí)評(píng)論回復(fù)的項(xiàng)目實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03springboot定時(shí)任務(wù)不起作用問(wèn)題及解決
文章主要介紹了Spring Boot中延遲加載bean的概念,并討論了如何解決定時(shí)任務(wù)不執(zhí)行的問(wèn)題,通過(guò)設(shè)置`@Lazy(false)`注解,可以指定某些類不使用延遲加載,從而解決定時(shí)任務(wù)無(wú)法執(zhí)行的問(wèn)題2024-11-11java字節(jié)碼框架ASM操作字節(jié)碼的方法淺析
這篇文章主要給大家介紹了關(guān)于java字節(jié)碼框架ASM如何操作字節(jié)碼的相關(guān)資料,文中通過(guò)示例代碼介紹的很詳細(xì),有需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-01-01Hibernate三種狀態(tài)和Session常用的方法
本文主要介紹了Hibernate三種狀態(tài)和Session常用的方法,具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-03-03Java使用Runnable接口創(chuàng)建線程的示例代碼
在Java中,多線程編程是實(shí)現(xiàn)并發(fā)操作的重要手段之一,通過(guò)多線程,程序可以同時(shí)執(zhí)行多個(gè)任務(wù),從而提高應(yīng)用程序的效率和響應(yīng)速度,Java提供了多種創(chuàng)建線程的方式,其中實(shí)現(xiàn)Runnable接口是最常見(jiàn)且推薦的方式之一,本文將詳細(xì)介紹如何使用Runnable接口創(chuàng)建線程2025-02-02Spring?Cloud?Alibaba實(shí)現(xiàn)服務(wù)的無(wú)損下線功能(案例講解)
這篇文章主要介紹了Spring?Cloud?Alibaba實(shí)現(xiàn)服務(wù)的無(wú)損下線功能?,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03Java多線程并發(fā)的指令重排序問(wèn)題及volatile寫屏障原理詳解
這篇文章主要介紹了Java多線程并發(fā)的指令重排序問(wèn)題及volatile寫屏障原理詳解,指令重排序是編譯器或處理器為了提高性能而對(duì)指令執(zhí)行順序進(jìn)行重新排列的優(yōu)化技術(shù),需要的朋友可以參考下2024-01-01