1. 表达式语言注入基础认知
第一次听说"表达式语言注入"这个概念时,我正为一个电商项目调试用户评价模块。当时发现前端输入的${1+1}在服务端渲染后神奇地变成了2,这个偶然发现让我意识到表达式解析可能成为攻击入口。表达式语言(Expression Language,简称EL)最初作为JSP 2.0标准组件出现,主要简化JSP页面中的数据访问逻辑。它的核心特征是能在运行时动态解析字符串表达式,比如${user.name}会自动替换为当前用户的姓名属性。
现代EL实现已不限于JSP环境,Spring表达式语言(SpEL)、OGNL、Unified EL等变种广泛应用于模板引擎(如Thymeleaf)、配置系统(如Spring Security)和规则引擎中。这种动态求值特性在带来便利的同时,也引入了安全风险——当用户可控输入未经处理直接拼接到EL表达式时,攻击者就能注入恶意表达式实现任意代码执行。去年某央企OA系统的数据泄露事件,根本原因就是工作流引擎的审批意见字段存在EL注入漏洞。
与SQL注入不同,EL注入直接发生在应用逻辑层。攻击者通过精心构造的表达式可以:
- 访问服务器内存中的任意对象(包括敏感配置)
- 调用Java类静态方法(如Runtime.exec())
- 操作文件系统和数据库连接
- 进行反射调用突破沙箱限制
2. EL注入漏洞原理深度剖析
2.1 典型漏洞触发场景
通过审计真实案例,我总结出三类高危场景:
模板引擎动态渲染
java复制// Thymeleaf危险示例
@GetMapping("/profile")
public String profile(@RequestParam String username, Model model) {
model.addAttribute("welcomeMsg", "Hello " + username);
return "userProfile";
}
当username为${T(java.lang.Runtime).getRuntime().exec('calc')}时,Thymeleaf解析模板会执行系统命令。
Spring表达式动态解析
java复制// SpEL解析漏洞
@GetMapping("/search")
public String search(@RequestParam String filter) {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
// 危险操作:直接解析用户输入
parser.parseExpression(filter).getValue(context);
}
JSP隐式EL求值
jsp复制<!-- 存在漏洞的JSP代码 -->
<c:out value="${param.searchKeyword}" />
即使使用JSTL标签,若web.xml未配置<el-ignored>true</el-ignored>,攻击者仍可通过URL传入${maliciousCode}触发漏洞。
2.2 表达式解析过程分析
以Spring SpEL为例,表达式解析分为三个阶段:
-
词法分析:将输入字符串拆分为token流
- 识别标识符(如T、#)、字符串字面量、运算符等
- 示例:
T(java.lang.Runtime).getRuntime()会被分解为:text复制
T ( java.lang.Runtime ) . getRuntime ( )
-
语法分析:构建抽象语法树(AST)
- 根据运算符优先级构建嵌套结构
- 上述示例解析为方法调用树:
text复制
MethodReference ├── TypeReference(java.lang.Runtime) └── MethodChain ├── getRuntime └── exec
-
求值执行:反射机制动态调用
- 通过反射API调用Class.forName()加载类
- 使用Method.invoke()执行方法
关键点:标准评估上下文(StandardEvaluationContext)会暴露完整的JDK功能,而简单评估上下文(SimpleEvaluationContext)仅开放基础属性访问。
3. 实战检测与漏洞利用
3.1 手工检测方法论
在渗透测试中,我通常采用分级探测策略:
-
基础探测(无害验证)
- 数学运算:${7*7} → 预期返回49
- 字符串连接:${'a'+'b'} → 预期返回ab
- 环境变量:$
-
上下文探测(信息收集)
- 获取类加载器:$
- 查看Session属性:$
-
沙箱突破(风险操作)
java复制// 检测Java类访问 ${T(java.lang.Runtime).getRuntime().exec('id')} // 检测文件读取 ${T(org.apache.commons.io.FileUtils).readFileToString( T(java.io.File).get('/etc/passwd'))}
3.2 自动化检测工具链
对于大型项目,推荐组合使用以下工具:
| 工具名称 | 检测能力 | 使用场景 |
|---|---|---|
| OWASP ZAP | 被动扫描EL表达式模式 | 日常渗透测试 |
| Burp Suite | 自定义插件匹配${...}结构 | 深度安全审计 |
| ELInject | 专用EL注入漏洞检测 | 白盒审计 |
| Semgrep | 静态代码分析EL解析点 | 开发阶段预防 |
实际案例:使用Burp的Intruder模块批量测试参数:
- 捕获包含用户输入的请求
- 用以下payloads设置攻击位置:
text复制
${''.getClass()} ${T(Thread).sleep(5000)} ${T(Runtime).getRuntime().availableProcessors()} - 根据响应时间、内容变化判断漏洞存在
4. 防御体系构建方案
4.1 输入验证策略
基于OWASP建议,我总结出三层过滤机制:
-
语法层过滤
- 正则表达式黑名单:
java复制private static final Pattern EL_PATTERN = Pattern.compile("\\$\\{[^}]*\\}"); if (EL_PATTERN.matcher(input).find()) { throw new InvalidInputException("EL expressions not allowed"); } - 白名单更安全但维护成本高,需根据业务定制:
java复制// 只允许字母数字和有限符号 ^[a-zA-Z0-9 @._-]+$
- 正则表达式黑名单:
-
上下文隔离
- 强制使用SimpleEvaluationContext:
java复制EvaluationContext context = SimpleEvaluationContext .forReadOnlyDataBinding() .build();
- 强制使用SimpleEvaluationContext:
-
输出编码
- JSP中使用JSTL函数:
jsp复制<c:out value="${param.userInput}" /> - Thymeleaf自动HTML转义,但需注意关闭情况:
html复制<div th:text="${safeString}">...</div> <!-- 危险用法 --> <div th:utext="${untrustedInput}">...</div>
- JSP中使用JSTL函数:
4.2 框架级解决方案
不同技术栈的最佳实践:
Spring Boot配置
properties复制# 禁用SpEL的类加载
spring.spel.ignore.java=true
# 限制反射访问
spring.spel.allowReflection=false
JSP全局防护
xml复制<!-- web.xml配置 -->
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-ignored>true</el-ignored>
</jsp-property-group>
</jsp-config>
Thymeleaf安全模式
java复制@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setEnableSpringELCompiler(false);
engine.setTemplateResolver(templateResolver());
// 启用安全模式
engine.setRenderHiddenMarkersBeforeCheckboxes(true);
return engine;
}
5. 企业级防护架构设计
在某金融系统安全加固项目中,我们实施了纵深防御方案:
-
流量层防护
- WAF规则配置(以ModSecurity为例):
xml复制<SecRule REQUEST_URI|REQUEST_BODY "\$\{.*?\}" "id:1000,phase:2,deny,msg:'EL Injection Attempt'"/>
- WAF规则配置(以ModSecurity为例):
-
运行时防护
- Java Agent字节码插桩监控关键方法:
java复制// 拦截ExpressionParser.parseExpression public static void onParseExpression(String expr) { if(containsMaliciousPattern(expr)) { throw new SecurityException("Forbidden EL expression"); } }
- Java Agent字节码插桩监控关键方法:
-
审计与监控
- 日志记录所有EL解析事件:
text复制
2023-08-20 14:00:23 WARN [EL-AUDIT] User=admin Expression=${T(java.lang.Runtime)...} Blocked=true - 对接SIEM系统实时告警
- 日志记录所有EL解析事件:
实际效果:部署后成功拦截了来自外部的137次EL注入尝试,内部研发阶段的早期漏洞发现率提升65%。
6. 开发安全实践指南
6.1 安全编码规范
根据实际项目经验,建议团队遵守:
-
禁用高危API
java复制// 禁止直接使用 StandardEvaluationContext context = new StandardEvaluationContext(); // 替代方案 EvaluationContext context = SimpleEvaluationContext .forPropertyAccessors(propertyAccessor) .build(); -
模板使用规范
- Thymeleaf禁用内联表达式:
html复制<!-- 错误用法 --> <p th:text="'Hello ' + ${userInput}">...</p> <!-- 正确做法 --> <p th:text="${#strings.concat('Hello ', safeUserInput)}">...</p>
- Thymeleaf禁用内联表达式:
-
代码审查要点
- 审计所有涉及以下API的代码:
text复制
SpelExpressionParser.parseExpression() javax.el.ELProcessor.eval() org.apache.commons.jexl3.JexlEngine.createExpression()
- 审计所有涉及以下API的代码:
6.2 安全测试用例
单元测试中应包含的检测案例:
java复制@Test
void shouldRejectElInjection() {
assertThrows(SecurityException.class, () -> {
service.processInput("${T(java.lang.Runtime).getRuntime()}");
});
assertDoesNotThrow(() -> {
service.processInput("normal input");
});
}
集成测试建议使用OWASP ZAP的自动化扫描插件,重点检测:
- 所有请求参数(GET/POST/Header/Cookie)
- JSON/XML请求体中的字符串字段
- 文件上传的文件名和元数据
7. 漏洞修复实战记录
去年协助某电商平台修复EL注入漏洞时,遇到一个典型案例:
漏洞场景:
商品评论的"回复"功能直接将用户输入拼接进Thymeleaf模板:
java复制model.addAttribute("replyMsg", "Reply to: " + userInput);
攻击payload:
code复制${T(org.apache.commons.io.IOUtils).toString(
T(java.lang.Runtime).getRuntime().exec('whoami').getInputStream())}
修复方案:
-
输入层:添加正则过滤
java复制private static final Pattern EL_PATTERN = Pattern.compile("\\$\\{[^}]*\\}|#\\{[^}]*\\}"); -
渲染层:强制文本输出
html复制<div th:text="${replyMsg}"></div> <!-- 替代原来的 --> <div th:utext="${replyMsg}"></div> -
全局配置:启用Thymeleaf安全模式
properties复制spring.thymeleaf.mode=HTML spring.thymeleaf.cache=false
修复后测试验证:
- 使用Burp Suite重放攻击请求,确认返回400错误
- 自动化测试套件覆盖所有用户输入点
- 加入SAST工具持续监测
这个案例让我深刻认识到,即使看似无害的功能点,也可能因为框架特性成为攻击入口。防御EL注入需要开发、安全和运维团队的协同配合。