SpringSecurity是一个流行的权限管理框架,类似Shiro,但功能更加完善。其中OAuth是一个提供安全认证支持的一个模块。用户使用Whitelabel Views处理错误时,攻击者在被授权的情况下可以通过构造恶意参数来远程执行命令
前往某网站下载Demo代码:http://secalert.net/research/cve-2016-4977.zip 环境搭建不复杂,Maven+SpringBoot项目,直接启动
观察启动文件:resources/application.properties
,观察到clientId是acme,密码是password
xxxxxxxxxx
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
xxxxxxxxxx
private 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,做表达式的解析
xxxxxxxxxx
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模板
xxxxxxxxxx
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.replacePlaceholders
,parseStringValue
的代码比较复杂。实现的功能是从template中找到helper定义的前缀和后缀,然后交给resolver处理,而resolver的处理逻辑正是上文PlaceholderResolver
中的表达式解析
xxxxxxxxxx
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可以被解析到
xxxxxxxxxx
protected String parseStringValue(String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
......
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
......
}
逻辑上大概是这样:从template中找到${
开头并且}
结尾的所有部分,第一次取到errorSummary,第二次取到errorSummary里面的表达式,成功操作命令执行,动态调试如下
SpelView构造方法中,加入了一个随机生成的前缀
xxxxxxxxxx
this.template = template;
this.prefix = new RandomValueStringGenerator().generate() + "{";
......
render方法中,随机前缀拼接到template之前,可以这样理解${errorSummary} -> random{errorSummary}
,由于没有递归加,所以payload没有加入random,执行前判断random,由于只有最外层符合,所以无法触发RCE
存在暴力破解的可能,因为random固定是六位。但没有价值,因为每执行一条命令都需要几万次的暴力破解请求
xxxxxxxxxx
String maskedTemplate = template.replace("${", prefix);
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(prefix, "}");
String result = helper.replacePlaceholders(maskedTemplate, resolver);
result = result.replace(prefix, "${");