SpEL 表达式注入

1、SpEL 基础

1.1 简介

在 Spring3 中引入了 Spring 表达式语言(Spring Expression Language,简称 SpEL),是一种强大的运行时查询和操作对象图的语言,语法类似于Jakarta表达语句,但额外支持方法调用和基本字符串模板。SpEL旨在为Spring社区提供一种统一且功能全面的表达式语言,适用于所有Spring产品,并根据这些产品的需要设计了其特性。尽管SpEL是Spring框架的一部分,但它可以独立于Spring使用。通常情况下,用户只需编写简单的表达式字符串即可利用SpEL的功能,无需关心底层架构细节。例如,在基于XML或注解的bean定义中集成SpEL就是一个常见应用。

SpEL 表达式必须使用占位符语法 #{SpelExpression},以便它们可以嵌入到纯文本字符串中(换句话说,SpEL 启用了表达式模板)。

SpEL 还可使用 @BeanID语法在注册表中查找 Bean (通常是 Spring 注册表)。例如,使用 ID、headerUtils 和方法 count () (计数当前消息中的标头数)指定 bean,可以在 SpEL predicate 中使用 headerUtils bean,如下所示:

1.2 表达式类型

1.2.1 字面值

最简单的 SpEL 表达式就是仅包含一个字面值。

下面我们在 XML 配置文件中使用 SpEL 设置类属性的值为字面值,此时需要用到 #{} 定界符,注意若是指定为字符串的话需要添加单引号括起来:

还可以直接与字符串混用:

1.2.2 Demo

img

参考 Drunkbaby 师傅的文章:

1.3 引用 Bean

SpEl 表达式能够通过其他 Bean 的 ID 进行引用,直接在 #{} 符号中写入 ID 名即可,无需添加单引号:

原来的写法:<constructor-arg ref="test"/>

在 SpEL 中:<constructor-arg value="#{test}">

1.3.1 Demo

SpellChecker.java

TextEditor.java

RefSpellAndEditor.java

editor.xml

img

1.4 引用类属性

SpEL 表达式能够访问类的属性。

比如, 参赛者 Drunkbaby 是一位模仿高手,Johnford 唱什么歌,弹奏什么乐器,他就唱什么歌,弹奏什么乐器:

key 指定 kenny<bean> 的 id value 指定 kenny<bean>的 song 属性。其等价于执行下面的代码:

1.5 引用类方法

SpEL 表达式还可以访问类的方法。

假设现在有个 SongSelector 类,该类有个 selectSong() 方法,这样的话 Drunkbaby 就可以不用模仿别人,开始唱 songSelector 所选的歌了:

carl 有个癖好,歌曲名不是大写的他就浑身难受,我们现在要做的就是仅仅对返回的歌曲调用 toUpperCase() 方法:

注意:这里我们不能确保不抛出 NullPointerException,为了避免这个讨厌的问题,我们可以使用 SpEL 的 null-safe 存取器:

?. 符号会确保左边的表达式不会为 null,如果为 null 的话就不会调用 toUpperCase() 方法了。

1.6 类类型表达式 T(Type)

在 SpEL 表达式中,使用 T(Type) 运算符会调用类的作用域和方法。换句话说,就是可以通过该类类型表达式来操作类。

使用 T(Type) 来表示 java.lang.Class 实例,Type 必须是类全限定名,但 ”java.lang” 包除外,因为 SpEL 已经内置了该包,即该包下的类可以不指定具体的包名;使用类类型表达式还可以进行访问类静态方法和类静态字段。

这里就有潜在的攻击面了 因为我们 java.lang.Runtime 这个包也是包含于 java.lang 的包的,所以如果能调用 Runtime就可以进行命令执行

在 XML 配置文件中的使用示例,要调用 java.lang.Math 来获取 0~1 的随机数:

简单来说,在 SpEL 中, T(Type) 用于 获取一个 Java 类的 Class 对象,例如:

T(java.lang.String) 返回 java.lang.String.class。返回 Class 后,就可以调用类的静态方法、获取类的静态字段等等,而 T(Type) 允许任意加载类、调用静态方法,那么也就可以执行敏感操作,例如:T(java.lang.Runtime).getRuntime().exec("calc"),如果上下文没有限制 SpEL 的访问类型,那么意味着:SpEL = 远程命令访问。

1.6.1 Demo

修改 Demo.xml

img

1.6.2 恶意调用并执行命令

修改 Value 中的类类型表达式的类为 Runtime 并调用命令执行方法:

img

2、SpEL 用法

SpEL 的用法有三种形式,一种是在注解 @Value 中;一种是 XML 配置;最后一种是在代码块中使用 Expression。

2.1 XML 配置

2.2 注解 @Value

这种形式的值一般是写在 properties 的配置文件中的。

2.3 Expression 用法

具体步骤如下:

1、创建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现; 2、解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为 Expression 对象; 3、构造上下文:准备比如变量定义等等表达式需要的上下文数据; 4、求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值;

主要接口:

  • 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 方法用于设置对象值。

2.3.1 Demo

程序会将这里传入 parseExpression() 函数的字符串参数 spel 作为 SpEL 表达式来解析,而无需通过 #{} 符号来注明

img

2.3.2 类实例化

类实例化同样使用 Java 关键字 new,类名必须是全限定名,但 java.lang 包内的类型除外。

img

3、SpEL 表达式注入漏洞

3.1 漏洞原理

SimpleEvaluationContext 和 StandardEvaluationContext 是 SpEL 提供的两个 EvaluationContext:

  • SimpleEvaluationContext : 针对不需要 SpEL 语言语法的全部范围并且应该受到有意限制的表达式类别,公开 SpEL 语言特性和配置选项的子集。
  • StandardEvaluationContext : 公开全套 SpEL 语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。

SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集,不包括 Java 类型引用、构造函数和 bean 引用;而 StandardEvaluationContext 是支持全部 SpEL 语法的。

由前面的 类类型表达式 知道,SpEL 表达式是可以操作类及其方法的,可以通过类类型表达式 T(Type) 来调用任意类方法。这是因为在不指定 EvaluationContext 的情况下默认采用的是 StandardEvaluationContext,而它包含了 SpEL 的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行。

例如 2.3 中的 Demo :

img

3.2 通过反射的方式进行 SpEL 注入

3.3 基础 Poc

除了常见的 Runtime 的命令执行方法,还有ProcessBuilder进行命令执行

3.4 基础 Bypass

3.4.1 常见的 Bypass 技巧

3.4.2 JavaScript Engine Bypass

使用 JS 引擎进行绕过

获取所有 JS 引擎信息

img

通过结果中的 Names,我们知道了所有的 js 引擎名称故 getEngineByName 的参数可以填 [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript],举个例子:

payload:

img

3.5 通过类加载器构造 PoC & Bypass

3.5.1 UrlClassloader

这个方法就是通过远程类加载

Windons:

目标:现在有一个 Spring Boot 服务,它的 /eval?exp= 路径下可以进行 SpEL 表达式注入,我们要通过注入构造 URLClassLoader 来远程加载类 http://127.0.0.1:8999/Exp.jar 达到执行恶意类的目的

1) 首先构造一个恶意类

弹出计算器

2) 编译打包为 Exp.jar

img

3) 用 python 本地启动 http 服务

img

4) 准备 Spring Boot SpEL 漏洞 Demo

img

5)做完准备工作,我们就可以构造 payload 进行测试了

原始 payload:

如果直接注入:

内容就会被 Tomcat 拦截, 从 Tomcat 8.5+ / 9+ 开始, { } [ ] ( ) " . 这些字符如果未编码 会直接被拒绝。

对 payload 进行 URL 编码:

编码之后再进行注入,就可以弹出计算器了

img

Linux:

1) 先准备恶意类并编译

img

2)借用 Windows 中写好的 SpringBoot 项目启动漏洞 Demo

img

3)启动本地 http 服务

img

4)nc 监听 2333 端口

img

5)传入恶意 payload

6)

img

connect to [127.0.0.1] from localhost [127.0.0.1] 48096

  • 有一个进程从 本地(localhost)
  • 使用随机端口 48096
  • 连接到了监听端口 2333

img

如图,nc 已经获取到反弹 shell

可以执行命令

3.5.2 AppClassLoader

利用 SpEL,直接用 AppClassLoader 加载本地已经存在的 class,并调用里面的代码。无需远程 URL

首先 T(java.lang.ClassLoader)获取 ClassLoader 类这个 Class 对象

T(java.lang.ClassLoader).getSystemClassLoader()获取系统类加载器,即 AppClassLoader,接着加载AppClassLoader中的一个类loadClass('java.lang.Runtime'),调用getRuntime()执行命令exec('calc')。

AppClassLoader和URLClassLoader的区别在于AppClassLoader加载的是本机 classpath 的类,本地攻击无需远程 URL;URLClassLoader可以加载任意 URL 的类,用于远程加载恶意类。

img

参考:

SpEL(Spring表达语言)表达式详述:

https://javacfox.github.io/2019/07/04/SpEL%EF%BC%88Spring%E8%A1%A8%E8%BE%BE%E8%AF%AD%E8%A8%80%EF%BC%89%E8%A1%A8%E8%BE%BE%E5%BC%8F%E8%AF%A6%E8%BF%B0/

SpEL表达式:

https://mrbird.cc/SpEL%E8%A1%A8%E8%BE%BE%E5%BC%8F.html

https://drun1baby.top/2022/09/23/Java-%E4%B9%8B-SpEL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/#%E9%80%9A%E8%BF%87-ClassLoader-%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8%E6%9E%84%E9%80%A0-PoC-amp-Bypass

http://101.36.122.13:4000/2025/03/25/SPEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/#H1gIO