Apache Shiro 简介
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。Shiro的优势在于轻量级,使用简单、上手更快、学习成本低。
Shiro反序列化原理:Apache Shiro框架提供了 RememberMe 功能,用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。因此攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞。
在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,这就为伪造 cookie 提供了机会。只要 rememberMe 的 AES 加密密钥泄露,无论 shiro 是什么版本都会导致反序列化漏洞
Shiro-550(CVE-2016-4437)
特征:返回包中包含rememberMe=deleteMe字段。
影响版本:shiro<1.2.24
环境搭建
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
注意导入代码部署的文件夹是samples/web
,不是web
。
打开shiro-shiro-root-1.2.4/pom.xml
文件,并把jstl依赖
版本改为1.2
使用IDEA打开项目,选择shiro-shiro-root-1.2.4文件夹,最后点击OK
IDAE会自动下载依赖项,下载完成后编辑运行配置,设置为Tomcat本地服务器运行,然后JRE选择Java8版本
然后点击部署,工件选择samples-web:war
或 samples-web:war exploded
最后点击运行即可,出现下面界面即代表配置成功
漏洞分析
加密流程分析
首先,分析Apache官方通告(https://issues.apache.org/jira/projects/SHIRO/issues/SHIRO-550?filter=allissues)提到CookieRememberMeManager,以此为入口点进行分析。
CookieRememberMeManager继承AbstractRememberMeManager
AbstractRememberMeManager实现了RememberMeManager接口中的多个方法,主要是密钥、加解密、序列化/反序列化相关功能。另外包括登录成功、失败的处理,查看调用发现在DefaultSecurityManager中
DefaultSecurityManager是登录处,只要登陆都会经过这里的Login方法,从这里开始分析。在这里下个断点,然后authenticate方法验证凭证的正确性,如果不正确就不走下面的onSuccessfulLogin方法:
F7步入后,F8步过forgetIdentity,if判断中的isRememberMe方法判断是否勾选RememberMe,如果没有就不走rememberIdentity,rememberIdentity是加密的关键方法:
F7进入rememberIdentity方法,调用getIdentityToRemember()
,作用就是获取用户名赋值给 principals。然后继续F7进入rememberIdentity
方法:
进到convertPrincipalsToBytes方法,会序列化principals对象,也就是登陆名称root字符串。
往下走,就到了加密的函数位置,会上步骤中序列化principals对象进行加密处理。getCipherService()获取加密方式AES/CBC/PKCS5Padding
在下一行代码的if判断中通过getEncryptionCipherKey进行获取密钥进行加密,那么重点分析下这段代码逻辑:首先进入getEncryptionCipherKey(其实所有功能都在AbstractRememberMeManager文件中)
发现getEncryptionCipherKey函数中直接返回了一个encryptionCipherKey属性值,也就是密钥。
encryptionCipherKey属性是AbstractRememberMeManager中定义的一个私有属性
那么需要找到赋值的位置,在setEncryptionCipherKey方法中
继续找调用setEncryptionCipherKey的位置,发现在setCipherKey方法中
继续找调用setCipherKey的位置,发现在构造方法中
构造方法的setCipherKey传参了一个属性值DEFAULT_CIPHER_KEY_BYTES,该值即加密密钥,直接默认固定写死在代码中。
最终可以顺推一下密钥生成获取的流程。
在回到getDecryptionCipherKey获取密钥的位置,进入encrypt函数传参密钥和原始明文进行加密
encrypt函数中首先随机生成16位的IV。
获取到IV后回到encrypt函数中执行encrypt函数执行加密处理
加密原始明文数据,并将IV拼接到加密后的数据前16位
到此加密流程已经结束,回到rememberIdentity方法中,会进入rememberSerializedIdentity方法,F7步入会跳到AbstractRememberMeManager的子类CookieRememberMeManager中,其实现了rememberSerializedIdentity方法。此方法会将上步骤中的IV和加密数据进行BASE64编码后存入Cookie字段的rememberMe中。
整体加密流程并不复杂:
1、序列化principals对象,对象中保存了登录用户名(root)
2、加密算法AES/CBC/PKCS5Padding,iv是16位随机生成的值
3、将1)中序列化后principals对象的值跟DEFAULT_CIPHER_KEY_BYTES进行AES加密,将2)中随机生成的iv拼接到加密后的字符串最前面
4、Base64编码生成Base64字符串,写入Cookie中rememberMe的值
解密流程分析
解密时就不一定涉及到登录了,那么就需要从请求发起时进行跟进。可从哪个位置开始下断点呢?
Javaweb是根据web.xml文件配置路由,通过该路由指定过滤器等,在web.xml里面看到以下配置:
说明访问根目录就要走ShiroFilter过滤器,那么跟进过滤器进行分析:
ShiroFilter继承于AbstractShiroFilter类,AbstractShiroFilter又继承于OncePerRequestFilter类
OncePerRequestFilter类就是开始的地方,无论是GET还是POST都会从这里开始,但是呢中间会有很多各种调用,会比较繁琐,部分无关的会直接跳过,到关键函数位置再细致分析:
为了分析简单,可以直接构造Get方法的Cookie头部rememberMe发包。
F7进入doFilterInternal方法,在createSubject位置继续F7进入
F7进入buildWebSubject方法
F7进入buildSubject方法
F7进入createSubject方法
可以看到关键的resolvePrincipals方法了,
F7进入resolvePrincipals方法,就会跳到DefaultSecurityManager.java文件中,也就是之前分析shiro登录加密最开始下断点的文件。
F7跟进getRememberedIdentity方法进去,继续往下跟getRememberedPrincipals方法
可以看到两个方法getRememberedSerializedIdentity
、convertBytesToPrincipals
,后面本不用太过于分析了,跟加密流程大体相当。
- getRememberedSerializedIdentity:获取cookie的RememberMe的值并进行Base64解码
- convertBytesToPrincipals:获取到的密文进行解密并进行反序列化
convertBytesToPrincipals方法中获取前16位作为iv进行解密:
防守侧分析
推荐一款由ABC_123大佬开发的蓝队分析研判工具箱。
1、正常登录的原始明文内容形式如下:
2、攻击流量解密后的内容
可以很明显发现恶意攻击的流量,rememberMe字段的值会很长,而且解密后存在很多可疑的反序列化类。
那么在日常分析过程中,在分析大量shiro流量时,可以先根据rememberMe字段的长度进行排序,优先分析长度最长的流量;然后对rememberMe字段进一步解密,分析原始内容,确定shiro攻击的真实性。
参考链接:
https://xxe.icu/shiro_deserialization_vulnerability.html#%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8