Shiro产品了解
Apache Shiro是⼀个强⼤且易⽤的Java安全框架,执⾏身份验证、授权、密码和会话管理。使⽤Shiro的 易于理解的API,您可以快速、轻松地获得任何应⽤程序,从最⼩的移动应⽤程序到最⼤的⽹络和企业应 ⽤程序。
基本特点如下:
- Authentication:身份认证/登录,验证用户是否具有相应的身份。
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户是否能做事情。
- Session Management:会话管理,即用户登陆后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的。
- Cryptographt:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
- Web Support: Web支持,可以非常容易的集成到Web环境。
- Caching:缓存,比如用户登陆后,其用户信息,拥有的角色,权限不必每次去查,这样可以提高效率
- Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去。
- Testing:提供测试支持。
- Run As:允许一个用户假装为另一个用户
- Remember Me:记住我,这是非常常见的功能,即一次登录后,下次再来的话不用登录了。
Shiro反序列化漏洞
漏洞原理介绍
影响版本:Apache Shiro<=1.2.4
shrio1.2.4从上到下依次为:
all:所有的Shiro JAR包
core:shiro核心依赖
sample:shiro示例
support:shiro支持
tools:工具
web:shiro Web项目
本次的漏洞点出现在Remerber Me功能模块上。特征判断:返回包中包含rememberMe=deleteMe字段。在请求包的Cookie中为remerberMe字段赋任意值,收到返回包的Set-Cookie中存在rememberMe=deleteMe字段,说明目标开启了shiro的rememberMe功能。
这里有两个需要注意的地方:
要在response中返回含有rememberMe的字段对应的请求中去构造poc。
获取cookie请求包中request的必须是登录提交的http请求。
shiro在登陆出提供了rememberMe这个功能,来记录用户登录的凭证,然后shiro会对用户传入的cookie进行解密并且反序列化,服务端接受rememberMe的cookie值后的操作时:
cookie中rememberMe字段内容->Base64解码->使用密钥进行AES-CBC解密->反序列化。
由于该版本AES加密的密钥Key被硬编码在代码里(漏洞能够被利用的本质),其大部分项目未修改默认AES密钥,这意味着攻击者只要通过源代码找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化,AES加密,Base64编码,然后将其作为cookie的Remerme字段值发送。shiro将数据进行解密并且反序列化,最终触发反序列化漏洞。
处理Cookie的类是CookieRememberMemanaer,该类继承自AbstractRemembrMeManage类,跟进该类,很容易看到AES的key。
kPH+bIxk5D2deZiIxcaaaA==
漏洞复现
- 入口
入口点在rememberMe的cookie字段处,勾选RememberMe。
只有在登入成功后才会生成cookie,此时可以将构造好的payload赋值给Cookie中的rememberMe字段。
- 生成序列化数据
假如项目中存在commons-collections4.0
1 | <dependency> |
借助反序列化利用工具生成序列化数据。
ysoserial项⽬源码:https://github.com/frohoff/ysoserial ,然后⾃⼰编译, 也可直接下载编译好的 release。
使用命令:
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2 “calc” > vv.txt
执行的cc链一定是靶场中能够利用的。
编写加密脚本:
特别要注意的是,密钥一定要与当前项目引用的密钥完全一致,否则漏洞无法触发。
python环境需要使用3,需要安装pip install pyCrypto.
脚本如下:
1 | #!/usr/bin/python3 |
3.复现
将poc.txt里面的东西拷贝出来,在shiro登录界面时(post请求)提交登录表单将cookie处remerbeMe改成poc.txt里面的反序列化数据。
AES加密简述
AES加密有AES-128、AES-192、AES-256三种,分别对应三种密钥长度128bits(16字节)、192bits(24字节)、256bits(32字节)。
AES加密属于分组密码算法,常见的分组加密模式有:ECB、CBC、CFB、OFB。AES算法在对明文加密的时候,并不是把整个明文一股脑的加密成一段密文,而是把明文拆分成一个个独立的明文快,每一个明文块长度128bit,叫做块大小(BlockSize),也就是说,每个分组为16个字节(每个字节8位)。
这些明文块经过AES加密器复杂处理,生成一个个独立的密文块,这些密文块拼接在一起,就是最终AES加密的结果。
加密过程
加密过程-部分方法
1 | org.apache.shiro.mgt.DefaultSecurityManager#login |
加密过程-login:
进入org.apache.shiro.mgt.DefaultSecurityManager#login,其中authenticate(token)方法根据tokne判断是否登入成功,此时rememberMe=true,首次认证登入后,会生成cookie。
进入org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin函数
org.apache.shiro.web.mgt.CookieRememberMeManager#forgetIdentity函数的作⽤是清除之前cookie 中的rememberMe字段的值
加密过程-设置remerberMe=deleteMe
接着进入org.apache.shiro.web.servlet.SimpleCookie#removeFrom⽅法,其中的addCookieHeader方法帮助添加响应包字段,首次登陆会设置remerberMe=deleteMe,MAX-age=0,来删除此cookie。
到org.apache.shiro.msg.AbstractRememberMeManage#onSuccessfulLogin函数,判断token中remerberMe,因为是true,所以进入remerberIdentity函数。而convertPRincipalsToBytes方法会将登入的用户主题序列化并AES加密后输出。
加密过程-序列化
跟进org.apache.shiro.mgt.AbstractRememberMeManager#convertPrincipalsToBytes,其中serialize()方法将接受的数据序列化。
跟进serialize方法,看到熟悉的writeObject方法,并将序列化后的数据流返回。
加密过程-AES加密
回到到org.apache.shiro.mgt.AbstractRememberMeManager#convertPrincipalsToBytes,跟进encrypt函数,会将获得的序列化数据进行AES加密。
进⼊org.apache.shiro.mgt.AbstractRememberMeManager#encryt,其中getCipherService()返回AES 加密对象,看到使⽤的算法为 AES,加密模式为 CBC,填充⽅式为PKCS5Padding,即 AES/CBC/PKCS5Padding
其中 getEncryptionCipherKey()⽤于获取加密密钥,返回的是Shiro默认硬编码的key,也是上文中提到的kPH+bIxk5D2deZiIxcaaaA==
。
将key带入encrypt方法加密。
最终将加密后的bytes数据回传给org.apache.shiro.mgt.AbstractRememberMeManager# convertPrincipalsToBytes⽅法中的bytes变量。
回到org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity,此时已经拿到加密后 的数据,接着跟进rememberSerializedIdentity⽅法
加密过程-base64编码
org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity⽅法,要对序 列化的字节数组serialized进⾏base64编码,然后返回到cookie中。
至此cookie生成过程结束,将cookie返回到了客户端。
解密过程
此时已经是登陆状态了,携带协商好的cookie再次发起请求,观察系统解析cookie的流程。
解密过程-部分方法
1 | org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals |
org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals ⽅法获取客户端数 据,跟进getRememberedSerializedIdentity⽅法.
解密过程-读取cookie
org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity,其中 的getCookie().readValue(),从请求中读取cookie
org.apache.shiro.web.servlet.SimpleCookie#readValue ⽅法,读取cookie中rememberme字段值
解密过程–base64解码
回到org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity⽅ 法,使⽤ Base64.decode() ⽅法对接收到的数据进⾏base64解码,输出byte数据
回到org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals,跟进 convertBytesToPrincipals ⽅法
解密过程-AES解密
org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals,其中decrypt()⽅ 法要对数据进⾏解密
org.apache.shiro.mgt.AbstractRememberMeManager#decrypt,跟加密时流程很像,其中 getCipherService()⽤来返回AES对象,看到使⽤的算法为 AES,加密模式为 CBC,填充⽅式为 PKCS5Padding,即 AES/CBC/PKCS5Padding,跟加密时⽣成的对象是⼀样的。这⾥的 getEncryptionCipherKey()⽤途⼀样,获取Shiro默认硬编码的key
解密过程-反序列化
最终解密后返回到org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals, 继续反序列化,进⼊deserialize()⽅法
跟进反序列化,org.apache.shiro.io.DefaultSerializer#deserialize,最终到达 java.io.ObjectInputStream#readObject。
反序列化完毕后,回到org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals, principals=root。到此,cookie认证成功。
补丁分析
这个漏洞⼀个很关键的点就在于AES解密的密钥,攻击者需要知道密钥才能构造恶意的序列化数据。所 以官⽅针对这个漏洞的修复⽅式将使⽤默认硬编码的Key改为⽣成随机的Key加密: https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848