Spring Security OAuth历史上爆出过一个CVE-2016-4977,本篇文章主要作为本人在学习和调试这个漏洞时候的过程记录。
官方对于CVE-2016-4977的描述比较模糊,大概讲了下漏洞触发的原理,我们只知道最终的原因是因为执行了SpEL表达式 https://pivotal.io/de/security/cve-2016-4977 。对比给出的利用poc,我们来理解下整个运行的流程。 http://localhost:8080/oauth/authorize?responsetype=token&clientid=acme&redirect_uri=${1-65535}
可以看到表达式是被执行了的。
对别补丁, 我们可以知道是org.springframework.security.oauth2.provider.endpoint.SelView.java处出了问题。 我们在关键位置下断点来调适我们的程序,经过调试之后发现在代码处String result = this.helper.replacePlaceholders(this.template, this.resolver)执行后,${1-65535}被执行 继续在此处下断点,其中内容是spring框架定义的一个模版 ,后面所有的异常信息网页都是基于这个模版去修改的。进入这个函数来到org.springframework.util.PropertyPlaceholderHelper里面,其中parseStringValue函数是整个异常网页内容的生成函数。 经过对整个函数的调试分析,可以理出函数的大致流程如下,关键代码有注释:
protected String parseStringValue(String strVal, PropertyPlaceholderHelper.PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) { StringBuilder result = new StringBuilder(strVal);// 新建一个string buffer ,第一次是默认的 <html><body><h1>OAuth Error</h1><p>${errorSummary}</p></body></html>**/ int startIndex = strVal.indexOf(this.placeholderPrefix);//获取result中${的位置,其实后面整个流程就是,将异常的信息替换掉${}中的内容,然后将其包装成html流 while(startIndex != -1) { int endIndex = this.findPlaceholderEndIndex(result, startIndex);//获取}结束的位置 if(endIndex != -1) { String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);// 提取出${}之间的字符 String originalPlaceholder = placeholder; if(!visitedPlaceholders.add(placeholder)) { throw new IllegalArgumentException("Circular placeholder reference /'" + placeholder + "/' in property definitions"); } placeholder = this.parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);//再次解析,通过递归调用一次性将${}中的字符提取出来 String propVal = placeholderResolver.resolvePlaceholder(placeholder);//将上面提到的${}之间的内容作为参数传给resolvePlaceholder生成异常信息 if(propVal == null && this.valueSeparator != null) { ... } if(propVal != null) { propVal = this.parseStringValue(propVal, placeholderResolver, visitedPlaceholders);//将生成之后的异常信息再次调用parseStringValue函数 result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal); ... } return result.toString(); }
SpEL表达式,当函数将error="invalid_grant", error_description="Invalid redirect: ${1-65535} does not match one of the registered values: [http://www.baidu.com]"中的1-65535提取出来并解析了。于是1-65535被执行了。这也符合了官方的描述。 整理整个流程如下:
1.首次进入时,系统以默认模版内容<html><body><h1>OAuth Error</h1><p>${errorSummary}</p></body></html>作为函数parseStringValue的参数。
可以看到它是将this.helper = new PropertyPlaceholderHelper(“${“, “}”);变成了 this.helper = new PropertyPlaceholderHelper( new RandomValueStringGenerator().generate() + “{“, “}”)换言之也就是将$先变成一个随机数,那么我们的${1-65535}无法被解析成SpEL,但是RandomValueStringGenerator().generate()是一个随机的6位数,理论上依旧存在被爆破的风险。假设我们爆破出来为123456,然后http://localhost:8080/oauth/authorize?responsetype=token&clientid=acme&redirect_uri=123456{1-65535}即可执行。