shiro rce漏洞分析

1
在最近这几年,我们在渗透的过程中经常会用到shiro的rce漏洞来打点,直到目前为止还经常会在一些项目或者HW中也会经常遇到shrio的rce,因此了解shiro的RememberMe反序列化导致的命令执行漏洞的原理是至关重要的,在本次分享中,我将和大家一起学习shiro RememberMe反序列化漏洞。

环境搭建

​ 我这里选择网上已经有人搭建好的漏洞环境来搭建环境https://github.com/potats0/ShiroDemo,下载好项目以后使用IDEA导入pom.xml文件,由于我这里使用的MAVEN是阿里云镜像,我使用shiro-core为1.2.4时提示我找不到包,因此我这里使用的是1.2.2版本。

image-20201128221450755

​ 需要的包导入后,找到App.java文件,通过调试来运行。

image-20201128221552395

​ 运行成功后界面如下:

image-20201128221658967

Shiro基本知识

​ 我们知道,shiro是一款用来进行权限认证和权限管理的框架,可以帮我们完成认证、授权、加密、会话管理、与Web集成、缓存等功能。

​ 下面我结合着这个漏洞环境的代码带大家一起学习一下shiro的基本知识。

​ 在这个项目的源码文件中,主要包含了4个文件,APP.java内容为启动springboot的内容,这里不做解释了,看下其他文件的内容,首先是MainRealm.java,在介绍这个文件的内容前,我们先了解几个基本的概念。

​ 我们知道,shiro框架的一个主要的功能是用来做身份认证的,在shiro中,主要通过principals (身份)和 credentials(证明)一起来验证用户的身份。

principals

​ 指用户身份的标识,可以是用户的用户名,手机号等等,但需要确保其唯一性。

credentials

​ 凭证,一般来说就是密码。

Realm

​ 域,shiro会从Realm中获取安全数据(用户,角色,权限),当SecurityManager要身份认证,需要从Realm中来确定用户身份以及用户可以访问的权限。

​ 在shiro中,SecurityManager负责身份认证的逻辑,它会委托给Authenticator进行身份认证,Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

AuthorizingRealm

​ 在shiro中,默认提供了一些Realm,他们的继承关系如下:

img

​ CachingRealm是带缓存的Realm,里面包含了多个CacheManager属性,具体的缓存在其子类中进行实现。

​ AuthenticatingRealm是带认证的Realm,该类实现了认证的基本逻辑和缓存逻辑。

​ AuthorizingRealm主要用来鉴权和获取授权信息的Realm,该类实现了Authorizer,所以可以做鉴权。也实现了PermissionResolverAware,RolePermissionResolverAware因此可以对用户的访问权限做判断。

image-20201129131100239

​ 在shiro认证过程中,依赖AuthenticatingRealm的getAuthenticationInfo方法,getAuthenticationInfo会调用我们自定义的doGetAuthenticationInfo方法获取认证的结果。

image-20201129132011123

​ 在登录的时候需要将数据封装到Shiro的一个token中,执行shiro的login()方法,Shiro就会自动的调用doGetAuthenticationInfo(AuthenticationToken token)方法获取身份认证信息,在本次环境中,首先通过token.getPrincipal()方法获取username信息,通过authenticationToken.getCredentials()获取密码信息,对用户的用户名和密码进行判断,如果用户用户名为admin,密码为vulhub,认证成功则返回SimpleAuthenticationInfo对象,mainrealm.java的代码如下:

image-20201129122749329

shiro拦截器

​ 在shiro中使用了与 Servlet 一样的 Filter 接口进行扩展,shiro拦截器的基础类及其继承关系如下:

img

​ ShiroFilter是整个 Shiro 的入口点,用于拦截需要安全控制的请求进行处理,除了上面的基础拦截器类外,shiro还提供了一些比较常用的默认拦截器。

​ FormAuthenticationFilter登录拦截器,它主要有两个作用,一个是拦截登录表单提交的路径,创建登录认证所需要的Token令牌,并进入登录认证流程。另一个作用是拦截要求登录后才可以访问的路径,如果已经登录则直接进入到要访问路径,如果未登录则访问被拒绝并跳转到登录页。登录拦截器常用的方法如下:

1
2
3
4
5
6
createToken创建认证令牌,令牌内存储了登录认证时所需的数据。
onLoginSuccess设置登录成功后的行为。
onAccessDenied设置被拒绝后的行为
setLoginUrl设置登录地址
getUsername获取登录名,表单name值必须是username。
getPassword获取密码,表单name值必须是password。

​ UserFilter用户拦截器,用户已经登录认证 或 已经记住我 的都可以通过。

​ AnonymousFilter无需认证即可通过。

拦截器链

​ Shiro 对 Servlet 容器的 FilterChain 进行了代理,即 ShiroFilter 在继续 Servlet 容器的 Filter 链的执行之前,通过 ProxiedFilterChain 对 Servlet 容器的 FilterChain 进行了代理。当Filter执行的过程中,首先执行shiro的拦截器链,再经过Servlet容器的拦截器链。

image-20201129143520958

​ 在shiro中提供了PathMatchingFilterChainResolver来判断请求的url和拦截器的规则是否匹配。

image-20201129143926553

​ DefaultFilterChainManager中维护者一个拦截器链,我们可以通过DefaultFilterChainManager中的方法添加拦截器。

image-20201129144850395

ShiroFilterFactoryBean

​ 通过ShiroFilterFactoryBean类可以方便的配置拦截器的各种基本属性。

1
2
3
4
5
6
setSecurityManager:注入一个SecurityManager类,SecurityManager负责管理整个shiro核心验证功能。
setLoginUrl:配置登录页路径。
setSuccessUrl:配置登录成功页路径。
setUnauthorizedUrl:配置没有权限跳转的页面。
setFilterChainDefinitionMap:设置拦截规则。拦截规则是通过一个Map进行导入的。
setFilters:用于注入自己实现的拦截器类。

​ 拦截规则是通过MAP来进行设置的,基本形式如下

1
2
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("<拦截路径>", "<拦截器名称>");

​ 拦截器路径是一个从根路径开始的url,并支持通配符。拦截器名称既可以是shiro内置拦截器的名称比如anon(无需认证的拦截器)、authc(需要认证的拦截器)、user (已经登录成功或使用记住我的拦截器),perms[role_name] - 需要权限验证的路径使用perms拦截器。中括号内为权限名称列表。


​ 接下来我们看一下shiroConfig.java,这个类是shiro的配置类,在这个类的shiroFilterFactoryBean中,通过setSecurityManager来设置securityManager,在securityManager中,设置了Realm为我们自己定义的mainRealm,RememberMeManager为cookieRememberMeManager,也就是cookie的”记住我”功能。通过setLoginUrl方法来设置未登录时需要认证的地址也就是登录地址。setUnauthorizedUrl方法来设置无权访问时跳转的地址。通过创建LinkedHashMap,设置map.put(“/doLogin”, “anon”)来设置不需要登录就能访问的地址。通过map.put(“/xxx/**”, “user”);来设置用户登录后才能访问的地址。最后通过setFilterChainDefinitionMap将这个map设置到FilterChain中。

image-20201129134331085

Subject

​ Shiro中认证授权组件Subject,为我们提供了当前用户、角色和授权的相关信息,可以进行登录,退出,权限验证,获取用户信息,session。

​ 通过SecurityUtils.getSubject获取subject对象

1
Subject subject = SecurityUtils.getSubject(); 

​ subject包含如下主要接口:

1
2
3
4
5
Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;    //登陆
void logout(Subject subject); //退出登陆
Subject createSubject(SubjectContext context); //获取subject
Session session = subject.getSession(); //获取session对象
String currentUser = subject.getPrincipal().toString(); //获取登录名

​ 了解了这些知识,我们看下UserController.java是如何实现的,通过SecurityUtils.getSubject方法获取subject对象,通过login来进行登录,传入的参数为UsernamePasswordToken对象。

image-20201129153131427

​ 这里,shiro的基础知识我们大概了解了一些了,下面我们再看一下漏洞。

漏洞分析

​ 因为之前已经有人分析过shiro的反序列化漏洞了,并且在文章中给出了存在反序列化漏洞的方法,在DefaultSerializer类的deserialize方法中,因此我们可以直接找到这个方法并打上断点 。

image-20201130114316497

​ 在这个函数中调用了ObjectInputStream类的readObject方法来进行反序列化操作,下面是整个过程的调用栈

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
deserialize:75, DefaultSerializer (org.apache.shiro.io)
deserialize:514, AbstractRememberMeManager (org.apache.shiro.mgt)
convertBytesToPrincipals:431, AbstractRememberMeManager (org.apache.shiro.mgt)
getRememberedPrincipals:396, AbstractRememberMeManager (org.apache.shiro.mgt)
getRememberedIdentity:604, DefaultSecurityManager (org.apache.shiro.mgt)
resolvePrincipals:492, DefaultSecurityManager (org.apache.shiro.mgt)
createSubject:342, DefaultSecurityManager (org.apache.shiro.mgt)
buildSubject:846, Subject$Builder (org.apache.shiro.subject)
buildWebSubject:148, WebSubject$Builder (org.apache.shiro.web.subject)
createSubject:292, AbstractShiroFilter (org.apache.shiro.web.servlet)
doFilterInternal:359, AbstractShiroFilter (org.apache.shiro.web.servlet)
doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:526, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:367, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:860, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1591, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

​ 下面我们分析一下这个漏洞的调用过程,再调用的过程中,我们可以看到拦截器的调用链,已经将shiroFilter写入到tomcat拦截器之前。

image-20201130155604254

​ 下面依次调用对应的拦截器,OrderedCharacterEncodingFilter–>OrderedFormContentFilter–>OrderedRequestContextFilter–>ShiroFilterFactoryBean–>WsFilter。

​ 首先调用OrderedCharacterEncodingFilter设置编码

image-20201130161156440

​ 调用OrderedFormContentFilter获取参数,这里参数为空,因此会调用else中的代码。

image-20201130161341690

​ 调用OrderedRequestContextFilter完成requestContext的初始化操作。

image-20201130162453138

​ 下来就是shiorFilter的调用链,再shior拦截器中调用了createSubject来创建subject对象。

image-20201130163400707

​ 跟进createSubject方法,调用了buildWebSubject方法。

image-20201130163533846

​ 在 buildWebSubject中调用了其父类的buildSubject方法。

image-20201130163606923

​ 跟进父类的buildSubject方法,调用了DefaultSecurityManager的createSubject方法。

image-20201130165013406

​ 继续跟进,我们需要关注resolvePrincipals方法。

image-20201130165132350

​ 跟进resolvePrincipals方法,判断RememberMeManager是否为空,不为空则调用getRememberedPrincipals。

image-20201130170513136

​ 跟进getRememberedPrincipals方法,首先调用了getRememberedSerializedIdentity。

image-20201130170719001

​ 跟进getRememberedSerializedIdentity方法,在该方法中,通过this.getCookie().readValue(request, response);获取了cookie的内容,并且判断是否为deleteMe,若为DeleteMe则返回null,否则将继续执行,对获取的cookie的内容进行base64解码并返回。

image-20201130171245277

​ 返回后会判断获取的cookie的内容是否为空,如果不为空,则调用convertBytesToPrincipals方法。

image-20201130171552207

​ 在convertBytesToPrincipals调用了decrypt对cookie的内容进行解密。

image-20201130171453470

​ 在decrypt中,调用cipherService.decrypt进行解密,同时传入了this.getDecryptionCipherKey()的内容。

image-20201130171804561

​ 我们看一下getDecryptionCipherKey中的key是如何来的,返回了当前对象的decryptionCipherKey属性。

image-20201130171919629

​ decryptionCipherKey是在setDecryptionCipherKey中设置的。

image-20201130172056694

​ 在setCipherKey中调用了setDecryptionCipherKey进行设置、

image-20201130172145175

​ setCipherKey中的参数来自于DEFAULT_CIPHER_KEY_BYTES,而这个值是kPH+bIxk5D2deZiIxcaaaA== base64解密后的内容,所以这个key的内容在我们当前的环境下是写死的。

image-20201130172223454

​ 继续跟进解密算法,可以看到使用的解密方式是AES方式。

image-20201130173904974

​ 将解密后的结果返回后,再转换为字节数组的形式并返回。

image-20201130174140275

​ 将返回的bytes作为参数传递到deserialize方法中。

image-20201130174259064

​ 继续跟踪下面的调用

image-20201130174337766

​ 最终我们可以看到,将我们通过cookie传入的内容转换为ObjectInputStream,并调用了readObject进行反序列化操作。

image-20201130174508646

​ 通过上面的分析,我相信我们已经对于shiro反序列化漏洞的基本原理有了大致的了解,这个漏洞要想正常的利用,至关重要的一点是找到解密cookie中传入payload的key,在这个环境中,key是写死的,实际上这个key也可以自己在shiorconfig类中配置加密的key。

image-20201130180331183

​ 当我们更换key后,再去利用这个漏洞,由于key不正确无法解密导致无法继续利用。

image-20201130180538186

​ 既然重新生成key这么简单,为什么还是有人使用默认的key或者网上公布的其他key?我们看一下这个key解密后的结果就知道了,由于这些key解密后都是一些乱码,不太容易编辑,所以可能很多人会去使用网上别人生成好的key,所以在漏洞利用的过程中有人想到了爆破key的方法。

image-20201130180951079

​ 那如果想自己生成key,怎样生成才能满足要求呢,只要使用任意一个16位,24位,32位的字符串base64编码后都是可以作为key的。

漏洞修复

​ 我们尝试切换shiro为高版本,看一下shiro是如何修复反序列化漏洞的,我切换到1.2.7版本的shiro,我们可以看到在该版本中,key默认并不是写死的,而是由cipherService.generateNewKey().getEncoded()来生成的key。

image-20201201164200537

​ 生成key的代码如下

image-20201201164541425

漏洞检测

如何识别shiro?

​ 要检测一个shiro是否存在反序列化漏洞,首先需要对shiro这个框架做一个识别,目前大部分的方法都是通过rememberMe=deleteMe;来识别的,但是并不是我们请求的所有地址都会返回rememberMe=deleteMe,但是在实战中我们可能需要批量去检测shiro框架,我们该如何识别?

​ 通过之前的分析我们知道,无论我们访问哪个url,都会经过shiro的拦截器,而在shiro的拦截器中会获取cookie中rememberMe的内容并进行解密,并且通过之前的分析,我们知道解密是由convertBytesToPrincipals()完成的,如果我们传入的rememberMe不能正常的反序列化,就会抛出异常,调用onRememberedPrincipalFailure方法。

image-20201201170537270

​ 该方法经过几层调用最终调用了this.getCookie().removeFrom(request, response);方法

image-20201201170614952

image-20201201170630060

image-20201201170639745

​ 在removeFrom中,会在返回包header加上rememberMe=deleteMe,因此无论我们访问任何使用了shiro框架的路由,只需要在请求头中的cookie中加入rememberMe=xxx,如果目标使用了shiro,则会返回rememberMe=deleteMe,也就是说我们可以通过一个包来识别是否使用了shiro框架。

image-20201201170725328

image-20201201170959832

​ 了解了这些,我们可以写一个简单的python脚本来批量识别shiro,这里注意我将重定向设置为false,否则我们在请求时会跟进302跳转,但跳转后的结果里可能没有rememberMe=deleteMe。
image-20201201180626070

image-20201201180604356

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
import requests

def shiroScan(url):

header={
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36',
'Accept':'image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
'Accept-Encoding':'gzip, deflate',
'Accept-Language':'zh-CN,zh;q=0.9',
'Cookie':'rememberMe=1',
'Connection':'close'
}
proxies = { "http": "http://127.0.0.1:8088", "https": "https://127.0.0.1:8088"}
resp=requests.get(url=url,headers=header,proxies=proxies,verify=False,allow_redirects=False)
for name, regex in resp.headers.items():
if "rememberMe=deleteMe" in regex:
print(url+ " is Shiro!!!")


if __name__ == "__main__":
with open('domain.txt','r') as f:
lines=f.readlines()
for i in lines:
domain=i.strip('\n')
shiroScan(domain)

image-20201201180705142

如何判断key是否正确?

​ 通过上面的测试我们知道当我们使用的加密key正确可以正常进行反序列化操作,即使key正确,但生成的内容无法正常反序列化,则还是会返回rememberMe=deleteMe,因为反序列化的操作是在convertBytesToPrincipals方法完成的,如果反序列化的过程中出现异常,还是会设置rememberMe=deleteMe这个请求头,因此我们测试key是否正确,需要找到一个可以不依赖第三方组件的java中默认存在且和jdk版本无关的类来进行反序列化。

image-20201202094802148

image-20201202094838261

​ 我尝试使用URLDNS来进行判断,虽然可以正常发起DNS请求,但 由于执行过程中的类型转换错误,因此还是会返回rememberMe=deleteMe。

image-20201202100922001

​ 我们正常的登录,看看正常登录过程中的反序列化的类是什么类型

image-20201202104733807

​ 使用这个rememberMe的内容利用,看shiro再进行反序列化的过程中反序列化的是哪个类?

image-20201202104854538

​ 跟进后发现是反序列化了SimplePrincipalCollection类,所以我们只要创建一个SimplePrincipalCollection对应的对象进行序列化就可以了,当传入的序列化内容可以正常被解析,就不会出现rememberMe=deleteMe,因此可以通过这个特性来判断我们的key是否正确。

image-20201202105111202

​ 我们查看一下ShiroExploit这个工具,看看他是怎么实现的。

image-20201202105408945

​ 这个工具在检测key是否正确的过程中调用了ysoserial的ShiroCheck,可我去查看ysoserial的github项目,发现并没有这个选项,也就是这个是作者自己扩展编写的。我们反编译ShiroExploit自带的ysoserial,他的实现也非常简单,就是创建一个SimplePrincipalCollection对象。

image-20201202105609290

如何检测利用链?

​ 在JAVA的反序列化漏洞中,仅仅找到readObject反序列化并不一定能造成RCE,还有一点比较重要的是需要找到利用链,这个要具体展开讲需要依赖的知识点有点多,我之后会单独写一篇文章来讲解。