根据Sping官方给出的公告信息,spring-security-oauth若干版本中包含一个RCE漏洞,恶意攻击者构造特定授权请求,当资源所有者将其转发给approval批准页面时可导致远程代码执行。
漏洞触发条件:
- 应用程序做为授权服务器角色 (例如使用注解@EnableAuthorizationServer)
- 使用默认的Approval Endpoint
实际开发中,大部分oauth2使用者一般都会重写Approval Endpoint以满足自身需求。所以初步看这个漏洞利用条件还是比较苛刻的。 受影响版本及详细可参考官方的公告:
官方的sample示例代码及网络上关于oauth2的demo不少,以Spring Security OAuth2 Demo工程为例新进行调试分析。作者给出了mysql表信息,创建数据库并添加一条测试数据即可以运行该demo,详细见sample代码库README描述。
可以看到这个demo启动类使用了@EnableAuthorizationServer注解,并且使用默认的授权批准页面。这里要说明的是authorization code模式的授权方式,如图所示: 用户发起授权请求时,client端将用户导向认证服务器,用户认证后进行approval授权批准,用户approval后,认证服务器将用户导向client端事先指定的重定向URIredirection URI),同时附上一个授权码,client端拿这个授权码向认证服务器申请访问令牌及刷新令牌,从而完成Oauth授权。在spring-security-oauth2中默认由请求/oauth/authorize处理授权请求,代码如下:
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
....
oauth2RequestValidator.validateScope(authorizationRequest, client);
....
return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal);
....
逐步往下看,其中createAuthorizationRequest函数处理授权请求参数并构造AuthorizationRequest对象,主要包含一下参数:
- response_type:表示授权类型,必选项,授权码模式此处的值固定为"code"
- client_id:表示客户端的ID,必选项
- redirect_uri:表示重定向URI,可选项
- scope:表示申请的权限范围,可选项
- state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
而后面的validateScope会对scope参数进行校验检查,如果clientScopes不为空则进行白名单检查,如果授权请求传入的scope不在设置的clientScopes这个list中,则抛出异常Invalid scope,如果clientScopes为空,则仅校验输入的scope不为空即会继续执行程序。而这个漏洞的触发点正式scope参数,所以在示例demo中一定要设置clientScopes为空,这也是这个漏洞触发的一个前提条件。validateScope的代码如下:
private void validateScope(Set<String> requestScopes, Set<String> clientScopes) {
if (clientScopes != null && !clientScopes.isEmpty()) {
for (String scope : requestScopes) {
if (!clientScopes.contains(scope)) {
throw new InvalidScopeException("Invalid scope: " + scope, clientScopes);
}
}
}
if (requestScopes.isEmpty()) {
throw new InvalidScopeException("Empty scope (either the client or the user is not allowed the requested scopes)");
}
}
最后调用getAuthorizationCodeResponse将请求forward至/oauth/confirm_access页面,由用户进行approval批准授权,代码如下:
// We need explicit approval from the user.
private ModelAndView getUserApprovalPageResponse(Map<String, Object> model,
AuthorizationRequest authorizationRequest, Authentication principal) {
if (logger.isDebugEnabled()) {
logger.debug("Loading user approval page: " + userApprovalPage);
}
model.putAll(userApprovalHandler.getUserApprovalRequest(authorizationRequest, principal));
return new ModelAndView(userApprovalPage, model);
}
这里userApprovalPage即为/oauth/confirm_access,model中包含了之前构造的授权请求AuthorizationRequest对象。ModelAndView简单说就是MVC框架中包含Model和View的对象,ModelAndView返回模型和视图后由DispatcherServlet解析处理该请求。随之程序进入/oauth/confirm_access,代码如下:
@RequestMapping("/oauth/confirm_access")
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
String template = createTemplate(model, request);
if (request.getAttribute("_csrf") != null) {
model.put("_csrf", request.getAttribute("_csrf"));
}
return new ModelAndView(new SpelView(template), model);
}
createTemplate函数创建模版视图,经过一些替换及拼接处理得到最终的template如下: 然后调用了SpelView初始化view对象。传入ModelAndView展示approval批准授权页面,之后DispatcherServlet解析处理,之后通过view.render调用加载视图。
try {
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" +
getServletName() + "'", ex);
}
throw ex;
}
因为这里的view是SpelView对象,所以进入SpelView类的render方法:
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
Map<String, Object> map = new HashMap<String, Object>(model);
String path = ServletUriComponentsBuilder.fromContextPath(request).build()
.getPath();
map.put("path", (Object) path==null ? "" : path);
context.setRootObject(map);
String maskedTemplate = template.replace("${", prefix);
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(prefix, "}");
String result = helper.replacePlaceholders(maskedTemplate, resolver);
result = result.replace(prefix, "${");
response.setContentType(getContentType());
response.getWriter().append(result);
}
调试跟进replacePlaceholders函数,可以看到函数调用了parseStringValue方法,并将模版中每一个变量值都传入resolvePlaceholder方法将输入变量做为Spel表达式去解析,resolvePlaceholder方法如下:
public String resolvePlaceholder(String name) {
Expression expression = parser.parseExpression(name);
Object value = expression.getValue(context);
return value == null ? null : value.toString();
}
其中expression.getValue会最终调用继承了MethodExecutor的ReflectiveMethodExecutor执行java.lang.Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator"),从而造成任意代码执行漏洞。理论上这里传入的name参数包括_csrf.token、client、path、scope等参数只要可控,都可以造成代码执行。
通过补丁可以看到官方把SpelView去掉,转而使用普通的View对象,同时对默认模版进行了一些修改。 Remove SpelView in WhitelabelApprovalEndpoint · spring-projects/spring-security-oauth@1c6815a · GitHub:https://github.com/spring-projects/spring-security-oauth/commit/1c6815ac1b26fb2f079adbe283c43a7fd0885f3d
通过分析可知,这个漏洞触发的前提条件较多,除文章开头官方给出的两个条件外,授权服务的scope也需要设置为空,这种情况在实际应用中非常少见。但这个漏洞的重点不是Oauth本身,而是Spel表达式使用带来潜在的安全问题。诸如SpelView等涉及spel表达式解析的接口应该还很多。只要参数外部可控均有可能造成任意代码执行。这个问题有点和Struts2的OGNL表达式相似。Spel表达式注入可能是Spring后面会面临较多的安全问题。挖漏洞也可以从这个方向入手。
- h3xStream's blog: Beware of the Magic SpEL(L) - Part 1 (CVE-2018-1273)
- Remove SpelView in WhitelabelApprovalEndpoint · spring-projects/spring-security-oauth@1c6815a · GitHub
- GitHub - wanghongfei/spring-security-oauth2-example: Spring Security OAuth2 Demo工程
- Spring Security OAuth
- 理解OAuth 2.0 - 阮一峰的网络日志
- spring-security-oauth/tests/annotation/approval at master · spring-projects/spring-security-oauth · GitHub