简单分析一下shiro550反序列化
shiro反序列化
环境下载
下载大佬写好的war包
https://cloud.tencent.com/developer/article/1472310
放到tomcat的路径webapps下,启动tomcat,就会自动解压
然后用idea打开文件夹
用IDEA内置tomcat部署即可
代码分析
前置条件
在漏洞的首页
https://issues.apache.org/jira/browse/SHIRO-550
在这里获取到的信息:
- CookieRememberMeManager这是一个入口
- 功能点是remember me 这个功能点
- 使用了AES 解密和 Base64加密
- 使用Java序列化(
ObjectInputStream
)反序列化 - 源代码存在默认的AES加密密钥,所有能够查看源代码的人都可以知道默认密钥是什么
首先进行测试一下正常功能点
我们进入login.jsp
进行查看,登陆的时候有一个rememeber me
功能点,勾选上
登陆之后在cookie中发现remember me字段
而shiro每次都会对cookie中的rememberMe来进行解密后反序列化操作来确定访问者权限,所以直接在cookie传输rememberMe参数就可以控制shiro反序列化的值
debug
第二个目的是获得加密解密的方法,以此来自行加密解密恶意payload进行传输
IDEA中按两次shift 搜索咱们前面准备当做入口点的CookieRememberMeManager类,按着函数列表查看后并未发现有关加密的信息,so跟进父类AbstractRememberMeManager去看一下
在这里前几行就有一个特殊的字符串
1 | private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="); |
根据前面我们在官网漏洞报告中得到的信息 这应该就是默认的AES密钥
查看这个参数在哪进行了使用
跟进setCipherKey(DEFAULT_CIPHER_KEY_BYTES)
1 | public void setCipherKey(byte[] cipherKey) { |
继续往下看 能找到一个encrypt函数,
1 | protected byte[] encrypt(byte[] serialized) { |
传入的serialized的值是我刚才web端登录的用户名root序列化后的数据
根据运行步骤函数名猜测流程是shiro验证完了登录的账号密码,然后根据用户名生成序列化数据准备进行加密了
在这里的cipherService
是一个新的对象,去查看他的源码
1 | org.apache.shiro.crypto.AesCipherService |
继续debug可以看到AES加密的模式
继续debug,跳转到org.apache.shiro.crypto.JcaCipherService#encrypt(byte[], byte[])
在这里的ivBytes是随机生成的长度为16的字节
1 | return this.encrypt(plaintext, key, ivBytes, generate); |
跳转至org.apache.shiro.crypto.JcaCipherService#encrypt(byte[], byte[], byte[], boolean)
传递的参数如下:
1 | plaintext没变 |
继续跟进
新建一个字节类型的参数名为output,长度为iv的长度加上encrypted长度
1 | System.arraycopy(iv, 0, output, 0, iv.length); |
这两行是进行了一个复制操作,将iv的内容复制到encrypted中,并赋值给output
所以output的值为随机生成的16位iv+AES加密结果
继续debug,发现传递的参数回到了最开始的encrypt、
继续debug,最终到了CookieRememberMeManager类中的rememberSerializedIdentity方法又对存储着加密结果的变量进行了一次base64加密,然后赋值到了cookie的rememberMe参数中,到此加密过程结束。
加密总结
最终总结加密过程为:
设定:密钥 = kPH+bIxk5D2deZiIxcaaaA==
1.获得明文 = 正常识序列化用户名后的字节(root)
2.以下步骤:
- 科普知识:正常的AES加密所需参数 = 想加密的字符串 + iv + key + CBC + padding
- shiro:AES加密 = 想加密的字符串 (
明文
) + iv(随机生成的长度为16的字节
) + key(base64解码**密钥**的结果
) + CBC + PKCS5Padding
3.随机生成的长度为16的字节 + AES加密结果 (就是拼接了一下)
4.base64加密
那么解密过程为:
设定:密钥 = kPH+bIxk5D2deZiIxcaaaA==
1.获得密文 = base64解密rememberMe参数传过来的值
2.以下步骤:
- 科普知识:正常的AES解密所需参数 = 想解密的字符串 + iv + key + CBC
- shiro:AES解密 = 想解密的字符串(
删除密文前16个字节的剩余字节
)+iv(密文的前16个字节
) + key(base64解码**密钥**的结果
) + CBC + PKCS5Padding
3.对解密结果进行反序列化,触发payload
解密部分
看一下最终触发反序列化的地方在哪里,按照加密方法调试过程,在解密方法org.apache.shiro.mgt.AbstractRememberMeManager#decrypt
处添加断点
其中serialized就是我们传入的cookie base64解码的结果
后面的步骤大致相似,直接到了org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals
这个函数
在这里进行了一种对象流的序列化
最后在调用readObject()完成了反序列化
解密时:从cookie中取出、base64解密、decrypt解密、反序列化
如何攻击
根据我们的分析来写poc
POC的加密流程就是:
- 获取到 反序列化的数据
- 设置AES加密模式,使用AES.MODE_CBC的分块模式
- 设置硬编码的 key
- 使用随机数生成 16 字节的 iv
- 使用 iv + AES加密(反序列化数据) 拼接
- 最后base64加密全部内容
命令执行还是需要cc链等利用链, shiro自带cc3.2.1,这一条是无法直接利用的