Java SPEL表达式注入漏洞原理解析

 更新时间:2023年10月31日 09:21:15   作者:郑瀚Andrew  
SpEL简称Spring表达式语言,在Spring 3中引入,SpEL能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,可以与基于XML和基于注解的Spring配置还有bean定义一起使用,本文给大家介绍Java SPEL表达式注入漏洞原理研究,感兴趣的朋友一起看看吧

Java SPEL表达式注入漏洞原理研究

一、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例子。

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表达式能够通过其他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 在求表达式值时一般分为四步,其中第三步可选:

  • 首先构造一个解析器: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。

官方文档:https://docs.spring.io/spring/docs/5.0.6.RELEASE/javadoc-api/org/springframework/expression/spel/support/SimpleEvaluationContext.html 

//关键类
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()

到此这篇关于Java SPEL表达式注入漏洞原理研究的文章就介绍到这了,更多相关Java SPEL表达式注入漏洞内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Mybatis返回类型为Map时遇到的类型转化的异常问题

    Mybatis返回类型为Map时遇到的类型转化的异常问题

    这篇文章主要介绍了Mybatis返回类型为Map时遇到的类型转化的异常问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • 基于Java语言的递归运算例题详解

    基于Java语言的递归运算例题详解

    一个方法在执行过程中调用自身, 就称为 "递归"。本文将通过几个例题带大家深入了解一下Java语言中的递归运算,感兴趣的可以了解一下
    2022-08-08
  • Java调用dll文件的实现解析

    Java调用dll文件的实现解析

    这篇文章主要介绍了Java调用dll文件的实现解析,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • Springboot使用CXF进行WSDL转换及WebService服务调用过程

    Springboot使用CXF进行WSDL转换及WebService服务调用过程

    本文介绍如何利用Apache CXF将WSDL转换为Java代码,提供bat命令和Maven插件两种方法,并详解生成类的属性(如name、targetNamespace)及调用接口的参数传递方式,助力高效集成Web服务
    2025-09-09
  • Java Set简介_动力节点Java学院整理

    Java Set简介_动力节点Java学院整理

    Set最大的特性就是不允许在其中存放的元素是重复的。接下来通过本文给大家分享java set常用方法和原理分析,需要的的朋友参考下吧
    2017-05-05
  • Java 中的自引用详解

    Java 中的自引用详解

    本文介绍了Java中“自引用”概念,即类中包含指向自身类型的成员变量,常用于链表、树等递归结构,自引用依赖于引用类型和JVM内存模型,避免无限递归,支持灵活安全的数据结构构建,并由GC自动管理内存,感兴趣的朋友跟随小编一起看看吧
    2025-10-10
  • RabbitMQ 延迟队列实现订单支付结果异步阶梯性通知(实例代码)

    RabbitMQ 延迟队列实现订单支付结果异步阶梯性通知(实例代码)

    这篇文章主要介绍了RabbitMQ 延迟队列实现订单支付结果异步阶梯性通知,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-02-02
  • java时区时间转为UTC的代码示例和详细解释

    java时区时间转为UTC的代码示例和详细解释

    作为一名经验丰富的开发者,我经常被问到如何将Java中的时间转换为UTC时间,这篇文章主要介绍了java时区时间转为UTC的代码示例和详细解释,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-09-09
  • Java基础之代码死循环详解

    Java基础之代码死循环详解

    这篇文章主要介绍了Java基础之代码死循环详解,文中有非常详细的代码示例,对正在学习java基础的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04
  • springboot starter介绍与自定义starter示例详解

    springboot starter介绍与自定义starter示例详解

    Spring Boot 的 Starter 是一组比较方便的依赖描述符,可以通过 Maven 将其打成jar包,并在你的项目中直接引用,这篇文章主要介绍了springboot starter介绍与自定义starter示例,需要的朋友可以参考下
    2025-05-05

最新评论