Spring Security OAuth RCE (CVE-2016-4977) 漏洞分析
Author: p0wd3r (知道创宇404安全实验室) Date: 2016-10-17
0x00 漏洞概述
1.漏洞简介
Spring Security OAuth 是为 Spring 框架提供安全认证支持的一个模块,在7月5日其维护者发布了这样一个升级公告,主要说明在用户使用Whitelabel views
来处理错误时,攻击者在被授权的情况下可以通过构造恶意参数来远程执行命令。漏洞的发现者在10月13日公开了该漏洞的挖掘记录。
2.漏洞影响
授权状态下远程命令执行
3.影响版本
2.0.0 to 2.0.9
1.0.0 to 1.0.5
0x01 漏洞复现
1. 环境搭建
1 |
docker pull maven |
1 2 3 4 5 6 7 8 9 10 11 |
FROM maven WORKDIR /tmp/ RUN wget http://secalert.net/research/cve-2016-4977.zip RUN unzip cve-2016-4977.zip RUN mv spring-oauth2-sec-bug/* /usr/src/mymaven WORKDIR /usr/src/mymaven RUN mvn clean install CMD ["java", "-jar", "./target/demo-0.0.1-SNAPSHOT.jar"] |
1 |
docker build -t mvn-spring . docker run --rm --name mvn-spring-app -p 8080:8080 mvn-spring |
2.漏洞分析
首先我们查看src/resources/application.properties
的内容来获取clientid
和用户的密码:
接着我们访问这个url:
http://localhost:8080/oauth/authorize?responsetype=token&clientid=acme&redirect_uri=hellotom
其中client_id
就是我们前面获取到的,然后输入任意用户名,密码填上面的password
。
点击登录后程序会返回这样一个页面:
可以看到由于hellotom
对于redirect_uri
来说是不合法的值,所以程序会将错误信息返回并且其中带着hellotom
,那么这个不合法的值可不可以是一个表达式呢?我们再访问这个url:
http://localhost:8080/oauth/authorize?responsetype=token&clientid=acme&redirect_uri=${2334-1}
结果如下:
可以看到表达式被执行,触发了漏洞。
下面看代码,由于程序使用Whitelabel
作为视图来返回错误页面,所以先看/spring-security-oauth/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/WhitelabelErrorEndpoint.java
中第18-40行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@FrameworkEndpoint public class WhitelabelErrorEndpoint { 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); } } |
这里定义了
Whitelabel
对错误的处理方法,可以看到程序通过oauthError.getSummary()
来获取错误信息,我们再次访问这个 url 并开启动态调试:
http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=${2334-1}
请求中的${2334-1}
已经被带入了errorSummary
中,然后errorSummary
被装入model
中,再用SpelView
进行渲染。
我们跟进SpelView
到spring-security-oauth/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/SpelView.java
中第21-54行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class SpelView implements View { ... 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(); } }; } ... public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { ... String result = helper.replacePlaceholders(template, resolver); ... } } |
render
时通过helper
取${}
中的值作为表达式,再用parser.parseExpression
来执行,跟进一下replacePlaceholders
这个函数,在/org/springframework/util/PropertyPlaceholderHelper.class
第47-56行:
1 2 3 4 5 6 7 8 |
public String replacePlaceholders(String value, final Properties properties) { Assert.notNull(properties, "\'properties\' must not be null"); return this.replacePlaceholders(value, new PropertyPlaceholderHelper.PlaceholderResolver() { public String resolvePlaceholder(String placeholderName) { return properties.getProperty(placeholderName); } }); } |
这个函数是个递归,也就是说如果表达式的值中有${xxx}
这样形式的字符串存在,就会再取xxx
作为表达式来执行。
我们看动态调试的结果:
首先因为传入了${errorSummary}
,取errorSummary
作为表达式来执行,继续执行程序:
由于errorSummary
中存在${2334-1}
,所以又取出了2334-1
作为表达式来执行,从而触发了漏洞。所以从这里可以看出,漏洞的关键点在于这个对表达式的递归处理使我们可控的部分也会被当作表达式执行。
3.补丁分析
可以看到在第一次执行表达式之前程序将$
替换成了由RandomValueStringGenerator().generate()
生成的随机字符串,也就是${errorSummary} -> random{errorSummary}
,但是这个替换不是递归的,所以${2334-1}
并没有变。
然后创建了一个helper
使程序取random{}
中的内容作为表达式,这样就使得errorSummary
被作为表达式执行了,而${2334-1}
因为不符合random{}
这个形式所以没有被当作表达式,从而也就没有办法被执行了。
不过这个Patch有一个缺点:RandomValueStringGenerator
生成的字符串虽然内容随机,但长度固定为6,所以存在暴力破解的可能性。
0x02 修复方案
- 使用 1.0.x 版本的用户应放弃在认证通过和错误这两个页面中使用
Whitelabel
这个视图。 - 使用 2.0.x 版本的用户升级到 2.0.10 以及更高的版本
- https://www.seebug.org/vuldb/ssvid-92474
- http://secalert.net/#CVE-2016-4977
- https://pivotal.io/de/security/cve-2016-4977
- https://github.com/spring-projects/spring-security-oauth/commit/fff77d3fea477b566bcacfbfc95f85821a2bdc2d
- https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.java