java反序列化

因为前面学习了java的一些基础知识,java反序列化也算是搁置了很久的知识点,所以就来学习一下关于这个java反序列化的知识点

参考文章和视频:

JAVA反序列化漏洞总结-青叶

java序列化与反序列化全讲解

Java反序列化漏洞专题-基础篇(21/09/05更新类加载部分)

什么是java反序列化

Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存或文件中,实现跨平台通讯和持久化存储,而反序列化则指把字节序列恢复为 Java 对象的过程。(这个的话在之前学ctfshow里头的反序列化篇也有详细的介绍过)

为什么需要序列化

我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

那么由此可以看出java序列化和反序列化的好处就是一是实现数据的存储二是实现数据的传输

序列化和反序列化的实现

实现方法

  • ObjectOutputStream类的 writeObject() 方法可以实现序列化。

  • ObjectInputStream 类的 readObject() 方法用于反序列化。

具体的实现方法

通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中,这就是序列化的过程。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可,也就是反序列化

我们先写个demo测试一下

Demo代码分析

先写个简单的类

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
import java.io.*;
//People类
public class People implements Serializable{

//类属性
public String name;
public int age;
public String sex;

//构造方法

public People(String name,String sex,int age){

this.name = name;
this.age = age;
this.sex = sex;
}

@Override
public String toString(){
return "People's name is " + name + "\n"
+"People's age is " + age + "\n"
+"People's sex is " + sex + "\n";
}

}

然后我们来写序列化和反序列化的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SerializeDemo {

//序列化操作
public static void main(String[] args) throws IOException, ClassNotFoundException {
System.out.println("----------序列化开始----------");
People p = new People("John", "Male", 30);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("People.txt"));
oos.writeObject(p);
oos.flush();
oos.close();

//反序列化操作
System.out.println("----------反序列化开始----------");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("People.txt"));
People p1 = (People) ois.readObject();
ois.close();
System.out.println(p1);
}
}

先看输出结果

image-20250526091421807

可以看到这里触发了toString()方法,是为什么呢?打个断点看看

image-20250526091531398

开始调试,可以发现执行到33行代码即实例化对象的时候会同时调用构造方法

image-20250526091722915

所以构造方法是在实例化对象的时候触发的,我们接下来执行代码

image-20250526091947755

发现代码在第44行的时候触发了toString()方法,这是因为我们直接将对象进行打印,这貌似跟PHP反序列化的__toString()方法的触发条件一样,假如这里改成System.out.println(p1.name)则不会触发toString()方法

代码走完了,我们来分析一下序列化和反序列化的操作

序列化分析

1
2
3
4
5
6
7
8
9
10
11
12
class SerializeDemo {

//序列化操作
public static void main(String[] args) throws IOException, ClassNotFoundException {
System.out.println("----------序列化开始----------");
People p = new People("John", "Male", 30);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("People.txt"));
oos.writeObject(p);
oos.flush();
oos.close();
}
}

先看看序列化的过程

在main方法中,这里先是实例化了一个有Serializable序列化接口的对象p,然后就开始进行序列化操作

  • new FileOutputStream("People.txt")

这里是实例化了一个java.io包中的FileOutputStream对象,该类用于将数据写入 File或 的输出流FileDescriptorFileOutputStream用于写入原始字节流(例如图像数据),并调用了该类的构造方法,用于将数据写入到指定文件中。这是一个写文件的操作,文件File就是我们指定的文件名or文件路径

image-20250526092748032

  • new ObjectOutputStream(new FileOutputStream("People.txt"));

实例化了一个java.io包中的 ObjectOutputStream 对象,用于将 Java 对象序列化并写入到输出流中。(只有支持 java.io.Serializable 接口的对象才能写入流。)

  • oos.writeObject(p);

这里调用了ObjectOutputStream 对象中的writeObject方法,用于将指定的对象写入ObjectOutputStream

image-20250526093916479

以上就是序列化的过程,ObjectOutputStream代表对象输出流,利用它的writeObject方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流FileInputStream指定的文件中。

然后我们再看看反序列化的过程

反序列化分析

1
2
3
4
5
6
//反序列化操作
System.out.println("----------反序列化开始----------");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("People.txt"));
People p1 = (People) ois.readObject();
ois.close();
System.out.println(p1.name);
  • new FileInputStream("People.txt"):创建一个 FileInputStream 对象,用于从文件 "People.txt" 中读取字节数据。
  • new ObjectInputStream(...):将 FileInputStream 包装为 ObjectInputStream,使其能够从字节流中反序列化对象。

反序列化的过程是从文件People.txt中读取输入流,然后通过ObjectInputStream将输入流中的字节码反序列化为对象,最后通过ois.readObject()将对象恢复为类的实例。然后我们看看关于readObject()方法

  • People p1 = (People) ois.readObject();

readObject()方法从 ObjectInputStream 读取一个对象。读取该对象的类、类的签名以及该类及其所有超类型的非瞬态和非静态字段的值。然后返回从流中读取的对象

image-20250526095011259

由于返回的对象类型是 Object,因此需要强制类型转换为 People

接下来我们看一下几个重要的函数和方法

分析函数

  • ObjectOutputStream构造函数

我们右键跟进该类的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;
writeStreamHeader();
bout.setBlockDataMode(true);
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}
  • bout = new BlockDataOutputStream(out);:创建一个 BlockDataOutputStream 对象,用于管理底层输出流。
  • enableOverride = false;enableOverride 用于控制是否允许子类重写 writeObject() 方法。如果为 true ,那么需要重写 writeObjectOverride 方法。但是一般默认为false

然后我们跟进writeStreamHeader()方法

该方法用于写入序列化流的头部信息。

1
2
3
4
protected void writeStreamHeader() throws IOException {
bout.writeShort(STREAM_MAGIC);
bout.writeShort(STREAM_VERSION);
}
  • STREAM_MAGIC 声明使用了序列化协议,bout 就是一个流,将对应的头数据写入该流中
  • STREAM_VERSION 指定序列化协议版本

看完了构造函数,我们接下来看看writeObject()方法都干了啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}

出现了一个if语句,这让我联想起刚刚写的enableOverride重写writeObject()的问题,但是默认是为false,所以这里是不会执行的

  • writeObject0(obj, false);这里的话就是执行序列化的方法了,跟进看一下
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
79
80
81
82
83
84
85
86
87
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
// handle previously written and non-replaceable objects
int h;
if ((obj = subs.lookup(obj)) == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}

// check for replacement object
Object orig = obj;
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
for (;;) {
// REMIND: skip this check for strings/arrays?
Class<?> repCl;
desc = ObjectStreamClass.lookup(cl, true);
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
if (enableReplace) {
Object rep = replaceObject(obj);
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}

// if object replaced, run through original checks a second time
if (obj != orig) {
subs.assign(orig, obj);
if (obj == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
}

// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}
  • desc = ObjectStreamClass.lookup(cl, true);这里通过调用lookup方法 查找类的描述信息,通过它就可以获取对象及其对象属性的相关信息。
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
if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
}

// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
}

然后这里的话就是根据 obj 的类型去执行序列化操作,如果为class类型则执行**writeClass方法并返回,如果为ObjectStreamClass 类型则执行writeClassDesc**方法等等,其最终结果都是往流中写入不同类型的对象

上面就是大致的序列化相干函数的分析,接下来我们看看反序列化的

  • ObjectInputStream构造函数
1
2
3
4
5
6
7
8
9
10
11
public ObjectInputStream(InputStream in) throws IOException {
verifySubclass();
bin = new BlockDataInputStream(in);
handles = new HandleTable(10);
vlist = new ValidationList();
streamFilterSet = false;
serialFilter = Config.getSerialFilterFactorySingleton().apply(null, Config.getSerialFilter());
enableOverride = false;
readStreamHeader();
bin.setBlockDataMode(true);
}

这个构造函数是用于初始化反序列化流并对一些基础配置进行设置的,跟上面的其实差不太多,我们跟进readStreamHeader()方法

1
2
3
4
5
6
7
8
9
10
protected void readStreamHeader()
throws IOException, StreamCorruptedException
{
short s0 = bin.readShort();
short s1 = bin.readShort();
if (s0 != STREAM_MAGIC || s1 != STREAM_VERSION) {
throw new StreamCorruptedException(
String.format("invalid stream header: %04X%04X", s0, s1));
}
}

这里是用于验证序列化流的头部信息的,通过读取序列化流中的序列版本和序列化协议检测头部信息是否有效,如果无效则会抛出异常

然后我们看看readObject方法

1
2
3
4
public final Object readObject()
throws IOException, ClassNotFoundException {
return readObject(Object.class);
}

跟进readObject方法,这个方法会从序列化流中读取一个对象,并将其强制转换为 Object 类型。

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
private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}

if (! (type == Object.class || type == String.class))
throw new AssertionError("internal error");

// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(type, false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
freeze();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}

前面的重写和判断类型就不说了,直接看后面的

  • Object obj = readObject0(type, false);这里从流中读取一个对象,进行一些异常处理和检测后返回读取的对象,之后关闭流并清理资源

跟进readObject0

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}

byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}

depth++;
totalObjectRefs++;
try {
switch (tc) {
case TC_NULL:
return readNull();

case TC_REFERENCE:
// check the type of the existing object
return type.cast(readHandle(unshared));

case TC_CLASS:
if (type == String.class) {
throw new ClassCastException("Cannot cast a class to java.lang.String");
}
return readClass(unshared);

case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
if (type == String.class) {
throw new ClassCastException("Cannot cast a class to java.lang.String");
}
return readClassDesc(unshared);

case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));

case TC_ARRAY:
if (type == String.class) {
throw new ClassCastException("Cannot cast an array to java.lang.String");
}
return checkResolve(readArray(unshared));

case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));

case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));

case TC_EXCEPTION:
if (type == String.class) {
throw new ClassCastException("Cannot cast an exception to java.lang.String");
}
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);

case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}

case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}

这个过程基本是跟 writeObject 差不多,我就不过多赘述了,只是介绍一个具体的思路以及分析源码的原理

一个小tip

之前在学习php反序列化的时候,我们都知道php反序列化使用的是serialize()序列化函数和unserialize()反序列化函数,那么我们也需要注意到,当类中存在__sleep()方法的时候,在我们序列化对象的时候就会触发该方法,当类中存在__wakeup()方法的时候,我们反序列化对象就会触发该方法。

java也是同理,readObject()就相当于php中的__wakeup()方法,而writeObject()就相当于__sleep()方法,其实这两个方法是默认可以不写的,但是我们可以通过重写这两种方法,通过自定义的writeObject()readObject()方法可以允许用户控制序列化和反序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

这个是我们反序列化攻击的重要思路,下面会详细介绍

Serializable 接口

Serializable 是java提供的标记接口,位于java.io包中,但是这个接口并没有任何内容(没有定义任何方法),它只是作为一个序列化能力的标识,意味着任何实现该接口的对象都可以实现序列化和反序列化的操作。

只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们删掉这个接口会是什么样的呢?

image-20250526113054640

另外我们需要注意的几个地方

  • 在反序列化的过程中,如果该类的父类没有实现序列化接口,那么就需要提供无参构造函数来重新创建对象
  • 一个实现序列化接口的子类是可以被序列化的
  • 静态成员变量不能被序列化(因为序列化是针对对象属性的,而静态成员变量是属于类本身的)
  • transient标识的对象成员变量不参与序列化

image-20250526115317085

这里可以看到,即使我们利用构造函数给test变量进行了赋值,但是在序列化并且反序列化的输出后发现该变量的值是null,意味着这个变量并没有参与序列化和反序列化的操作

Java反射

参考文章:

https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html

https://pdai.tech/md/java/basic/java-basic-x-reflection.html

在学习Java反射之前,先看看官方对反射的介绍

JAVA反射机制是在程序运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

Java 反射(Reflection)是一个强大的特性,它允许程序在运行时查询、访问和修改类、接口、字段和方法的信息。反射提供了一种动态地操作类的能力,这在很多框架和库中被广泛使用

在学习反射之前,我发现很多文章都会从一个很巧妙的角度去进行解释,那就是从正射出发引导反射的概念和利用

什么是正射呢?

很简单,就是我们对类的实例化的过程就是正射

1
People people = new People();

此时我们是知道这个类的信息例如成员变量成员方法,这个类是用来干嘛的,那么我们就可以进行实例化一个对象也就是正射

那如果我们并不知道类的相关信息呢?

所以此时就引出了反射的作用,反射就是当我们不知道我们实例化的对象是什么的时候,我们利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。讲的官方一点:Java反射(Reflection)是一种在运行时动态分析或修改类、对象、方法和属性的机制。它允许程序在不需要提前知道类具体结构的情况下,通过名称或对象实例来操作其内部成员。

Java的反射提供了一系列的类和接口来操作class对象,主要的类包括

  • **java.lang.Class**:表示类的对象。提供了方法来获取类的字段、方法、构造函数等。
  • **java.lang.reflect.Field**:表示类的字段(属性)。提供了访问和修改字段的能力。
  • **java.lang.reflect.Method**:表示类的方法。提供了调用方法的能力。
  • **java.lang.reflect.Constructor**:表示类的构造函数。提供了创建对象的能力。

Class类

每个java类运行时都在JVM里表现为一个class对象,对每一种对象,JVM 都会实例化一个 java.lang.Class 的实例,java.lang.Class 为我们提供了在运行时访问对象的属性和类型信息的能力

Class类一般来说是用来获取类对象的,获取对象的方法有三种

  • 根据类名:类名.class
  • 根据对象:对象.getClass()
  • 根据全限定类名:Class.forName(全限定类名)

在Java中,通过反射获取Class类对象以及类中的成员变量,方法和构造方法的话,我们需要用到Class类以及java.lang.reflect类库一起实行反射技术在反射包中,我们常用的类主要有Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private)

接下来我们看看如何通过反射操作类

通过反射实例化对象

  • Class::forName()方法
1
private static Class<?> forName(String className, Class<?> caller)

这个方法用于根据类名加载类,参数是要加载的类的全限定名(包括包名),而返回类型是加载的类对象Class

  • Class::getClass 方法

getClass 方法是一个无参函数

因为每个对象运行时都会存在一个Class实例,所以如果我们已经拿到了一个对象,可以很方便地使用它的 getClass 方法获得一个 Class

  • Class::getConstructor()/Class::getConstructors()方法
1
2
public Constructor<T> getConstructor(Class<?>... parameterTypes)
public Constructor<?>[] getConstructors()

这个方法用来获取类的所有公共构造函数,并返回一个Constructor<?> 数组,但是需要注意的是这里的参数是class类型的参数,例如我们的有参构造函数的参数是String类型的参数,那么我们在获取的时候就需要传入String.class类型的参数

  • Class::getDeclaredConstructor()/Class::getDeclaredConstructors()方法
1
2
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
public Constructor<?>[] getDeclaredConstructors()

其实和上面不同的是,这里能获取所有类型的构造函数

一个师傅的图片

shixinzhang

但是一般在获取类构造方法之前,我们需要先获取到类变量,下面我们会讲到

通过反射操作类变量

首先就是获取类变量

  • Class::getFields()方法/Class::getField()方法
1
2
public Field getField(String name)
public Field[] getFields()

返回一个包含某些Field对象的数组/单个Field对象,这些对象反映此Class对象所表示的类或接口的所有可访问公共字段。

  • Class::getDeclaredFields()/Class::getDeclaredField方法
1
2
public Field getDeclaredField(String name)
public Field[] getDeclaredFields()

返回 Field 对象的一个数组/单个Field对象,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段。包括公共、保护、默认(包)访问和私有字段,但不包括继承的字段。

继续放大师傅的图片讲的很简单明了

shixinzhang

然后就是对类变量的操作

  • Field::set()方法

其实这个本质上不是Class类中的方法,而是一些Field类的方法,这个方法主要是用来改变属性的值

1
public void set(Object obj,Object value)

这里需要传入一个对象和修改的值,并没有返回值

  • Field::setAccessible()方法

和getDeclaredFields()方法一样,这里的话相对于set方法来说权限更大,一般来说private属性的值是不可以被修改的,但是通过这个方法可以允许我们对私有属性设置操作权限

1
public void setAccessible(boolean flag)

例如我们类中age是一个private类型的成员变量

1
2
3
agefield.setAccessible(true);
agefield.set(p, 30);
System.out.println(p);

我们写个demo试一下

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
import java.lang.Class;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionTest {
public static void main(String[] args) throws Exception {
Class c = Class.forName("People");
Constructor ctor = c.getDeclaredConstructor(String.class, int.class);
People p = (People) ctor.newInstance("wang",30);
System.out.println(p);

//获取类的成员变量

//1.通过getFeild获取单个成员变量
//只能获取公共属性的成员变量
//Field nameField = c.getField("name");

//2.通过getFeilds获取所有公共属性的变量数组
//Field[] fields = c.Fields();

//3.通过getDeclaredField获取单个属性对象
//无论是private还是public的属性都能获得
Field nameField = c.getDeclaredField("name");
Field ageField = c.getDeclaredField("age");

//4.通过getDeclaredFields获取所有属性的变量数组
//Field[] fields = c.getDeclaredFields();

//操作类的成员变量
//公共类型可以直接操作
nameField.set(p,"meng");
ageField.setAccessible(true);
ageField.set(p,20);
System.out.println(p);
}
}

通过反射操作类方法

首先就是获取类方法

  • **getDeclaredMethod\getDeclaredMethods()**方法
1
2
public Method getDeclaredMethod(String name,Class<?>... parameterTypes)
public Method[] getDeclaredMethods()

返回一个 Method 对象/Method 对象的一个数组,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。name 参数是一个 String,它是方法的名称,parameterTypes 参数是 Class 对象的一个数组,它按声明顺序标识该方法的形参类型。前者是获取单个成员方法,后者是获取一个成员方法数组Method[]

  • **getMethod\getMethods()**方法
1
2
Method[] getMethods()
Method getMethod(String name, Class<?>... parameterTypes)

返回一个 Method 对象/Method对象数组,它反映此 Class 对象所表示的类或接口的指定公共成员方法。前者是获取单个成员方法,后者是获取一个成员方法数组Method[]

然后就是操作类,调用Method的invoke方法

  • Method::invoke()方法
1
public Object invoke(Object obj,Object... args)

obj–实例对象 …args–用于方法调用的实参

对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。

  • Method::setAccessible()方法
1
public void setAccessible(boolean flag)

接下来我们写个demo

  • 操作void返回值的方法
1
2
3
public void getName(String name) {
System.out.println("name's = " + name);
}

然后我们调用该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.Class;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionTest {
public static void main(String[] args) throws Exception {
Class c = Class.forName("People");
Constructor ctor = c.getDeclaredConstructor(String.class, int.class);
People p = (People) ctor.newInstance("wang",30);
//System.out.println(p);

//获取类的成员方法
Method m = c.getDeclaredMethod("getName",String.class);

m.invoke(p, "wang");
}
}
//name's = wang
  • 操作有返回值的方法
1
2
3
public String getName(String name) {
return name;
}

然后我们调用该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.Class;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionTest {
public static void main(String[] args) throws Exception {
Class c = Class.forName("People");
Constructor ctor = c.getDeclaredConstructor(String.class, int.class);
People p = (People) ctor.newInstance("meng",30);
//System.out.println(p);

//获取类的成员方法
Method m = c.getDeclaredMethod("getName",String.class);

String name = (String) m.invoke(p, "wang");
System.out.println(name);
System.out.println(p);
}
}

其实没啥区别,有返回值的话就需要一个变量去接收这个返回值,但是注意这里需要强制类型转化

反序列化漏洞的成因

我们先回忆一下在php反序列化中的漏洞成因是什么,其实就是在反序列化的时候由于我们反序列化函数接收的字符串是可控的,我们通过构造字符串导致在反序列化的时候自动触发一系列的方法最终导致达到我们想要的攻击效果例如读取文件,写木马等

然后我们回到java反序列化,其实跟readObject有关系,因为这个方法可以经过开发者去重写

只要服务器能反序列化数据,我们传入类的readObject中的代码就会自动执行从而达到一个攻击的效果

漏洞利用思路

经过上面的研究,我们不难想到,在构造链子的时候,readObject方法可以像PHP中的__destruct()或者__wakeup()方法一样会在反序列化操作的时候自动调用,所以也就作为链子的入口了

利用思路

  • 入口类的readObject方法直接执行危险代码调用危险方法
  • 入口类包裹其他类,即参数中包含其他可控的类,然后该类有危险的方法,通过readObject调用
  • 类包裹类,层层包裹,不过也是在readObject调用
  • 通过类加载机制构造函数/静态代码,等类加载的时候隐式执行代码

我们挨个解释一下

思路一

前面其实也讲过,readObject方法是可以重构的,并且在反序列化的时候会自动调用,所以我们直接在readObject方法中构造危险代码,从而调用危险方法

写个demo

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
import java.io.*;

//定义一个可序列化的类
public class People implements Serializable {

//成员属性
public String name;
public int age;

//有参构造函数
public People(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "People{"
+ "name = " + name + ",\t"
+ "age = " + age + "}";
}

//重写readObject方法
private void readObject(java.io.ObjectInputStream ois ) throws IOException, ClassNotFoundException {

//先执行默认的writeObject()方法
ois.defaultReadObject();

//重构方法
Runtime.getRuntime().exec("calc");
}
}

序列化并反序列化后发现这里先默认执行原先的readObject方法,然后再执行calc系统命令也就是启动计算器,然后我们分步调试看看过程

image-20250526151525675

步入readObject方法

image-20250526151604915

发现原生类的readObject方法并没有被调用,而是调用了用户重构的readObject方法,所以此时会执行27行和30行代码,也就会执行calc命令

从上面的例子可以看出,在执行unserialize反序列化操作后,readObject方法会被调用,并且如果readObject被用户重构则会调用新的readObject方法,所以我们可以利用这个思路进行一个任意代码执行

但是这种情况基本上不可能出现,除非开发者真的很粗心大意!

思路二

这里的话其实