boxmoe_header_banner_img

梦中的登神殿,天才的乱葬岗

加载中

文章导读

Java安全开发基础&原生反序列化&重写方法&链条分析&触发类&类加载


avatar
RootReapers 2026年2月12日 102

先澄清概念:

输入流:从硬盘等存储介质输入到JVM

输出流:从JVM输出到硬盘等存储介质


序列化:将JVM内存中的对象压缩成字节流

**反序列化:将字节流转换成JVM内存中的对象 **

为什么对象要序列化?

众所周知,对象是类的实例

类长啥样?(懒得敲完整的Java代码,这里我就用伪代码表示了,看得懂就好)

class user{
  String name;
  int age;
  String job;
  void info(){
    print(name,age,job)
  }
}

类通过构造方法实例化之后,对象得包括类里面的属性

暂时把对象的格式看成和类一样((对象具体是什么格式我也不清楚)


假如此时要把这个user存入数据库

怎么存?

整个搬进去?

众所周知,数据库存储数据向来就是单行的

比如

id username password

1 admin 123456


那对象怎么存?

一大堆换行符

总不能硬塞吧,你硬塞的话,就算程序不报错,你敢保证你的程序员同事不会给你来一脚吗,整个对象的结构都乱了


所以说研究出序列化和反序列化的目的就是为了便于传输

简单来说序列化就是把对象压缩成字符串

反序列化就是把序列化字符串还原为对象


流程

** 文件**

JAVA对象->(经过序列化)->字节流-> 网络传输

** 数据库**

这是一个可序列化&反序列化的类

package org.example.xiaodi_javasec.day04;

import java.io.Serializable;

public class User implements Serializable {
    public String name="rootreapers";
    public String gender="no_gay";//不是gay
    public int age=20;

    public User() {
        System.out.println("无参构造方法被调用");
    }

    public User(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        System.out.println("有参构造方法被调用");
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                '}';
    }
}

进行序列化/反序列化操作需要做到什么?

实现Serializable接口



这是一个序列化与反序列化的演示

package org.example.xiaodi_javasec.day04;

import java.io.*;

public class SerializableUser {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User user = new User();
        SerializableUser_test(user);//序列化
        unSerializableUser_test("ser.txt");
        System.out.println(unSerializableUser_test("ser.txt"));

    }
    public static void SerializableUser_test(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
        oos.writeObject(obj);
        //向ser.txt写入对象的序列化数据
    }
    public static Object unSerializableUser_test(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream oIs = new ObjectInputStream(new FileInputStream(Filename));
        Object o = oIs.readObject();
        return o;
}
}


定义了两个函数,一个是向文件写入序列化数据

另外一个是从文件读取序列化数据并且反序列化


 sr %org.example.xiaodi_javasec.day04.User洮辙嵗 I ageL gendert Ljava/lang/String;L nameq ~ xp   t no_gayt rootreapers

这是序列化数据,看起来像是不可读的形式

只能大概获取一些信息


分析一下序列化与反序列化函数

序列化

 public static void SerializableUser_test(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
        oos.writeObject(obj);
        //向ser.txt写入对象的序列化数据
    }

因为是写文件,所以不需要返回值,设成了void类型

接收一个Object(对象)类型的参数


创建一个ObjectOutputStream类的对象oos

并且在调用构造方法时,将一个新建的FileOutputStream对象作为参数传入


进入ObjectOutputStream看一下构造方法

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;
        }
    }

它的参数类型是一个输出流类(OutputStream)

而刚好,FileOutputStream继承了OutputStream

所以它可以作为参数传入


FileOutputStream调用单个参数的构造方法时

会调用File类单个参数的构造方法,在FileOutputStream的构造方法中,文件名name作为字符串类型的参数传入,FileOutputStream把参数传递给File

name来到File类的构造方法,同样作为参数传入,在File类中传入文件名作为参数,会创建一个对应的文件

那么也就是说,FileOutputStream其实就是创建了一个文件ser.txt

提供给ObjectOutputStream写入序列化数据


为什么不能直接调用File呢?因为File自身没继承OutputStream

而ObjectOutputStream需要的参数类型是OutputStream

所以只能通过FileOutputStream创建文件


然后是下一行,写入序列化数据

oos.writeObject(obj);

调用了ObjectOutputStream对象oos的writeObject方法

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;
        }
    }

进入writeObject内部

没有重写,所以if分支先不看

在try代码块中调用了writeObject0

再进一层

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);
        }
    }

好长的代码,但是大多数是不用看的

看try分支

else if (obj instanceof Class) {
                writeClass((Class) obj, unshared);
                return;
            }

这里判断传入的obj的是不是Class的实例

oos.writeObject(obj);

obj对应的是我们要写入的对象

那就肯定是Class的实例(一切类都继承于Class)

进入代码块,调用了writeClass,将obj转为Class类

这里就不用再跟下去了,再下一层就是序列化的逻辑(写入的内容是如何生成的)了


回到oos.writeObject(obj);

oos.writeObject(obj);

层层调用,写入了obj的序列化数据


运行,可以看到项目根目录下有ser.txt文件

 sr %org.example.xiaodi_javasec.day04.User洮辙嵗 I ageL gendert Ljava/lang/String;L nameq ~ xp   t no_gayt rootreapers

长这样


序列化完成

接下来是反序列化

public static Object unSerializableUser_test(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream oIs = new ObjectInputStream(new FileInputStream(Filename));
        Object o = oIs.readObject();
        return o;
}

反序列化是需要读取文件,并且还原为对象,然后返回对象

所以它的返回值类型是Object

ObjectInputStream oIs = new ObjectInputStream(new FileInputStream(Filename));

这里和输出流差不多,不细说了,刚才是创建,现在是读取

Object o = oIs.readObject();

进入readObject

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

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

奇了怪了这里怎么这样写?可能我水平不够看不懂

final修饰后不可重写

但是又校验是否重写

直接从try开始看

核心代码如下

Object obj = readObject0(false);

读对象,有个false参数

进readObject0看一下

private Object readObject0(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:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

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

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    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);
        }
    }

false不用管

这个函数主要是定义怎么读序列化数据并且反序列化的

会返回一个Object类型的数据

public static Object unSerializableUser_test(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream oIs = new ObjectInputStream(new FileInputStream(Filename));
        Object o = oIs.readObject();
        return o;

用o接收,然后直接return o就行

完整代码再贴一下

package org.example.xiaodi_javasec.day04;

import java.io.*;

public class SerializableUser {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User user = new User();
        SerializableUser_test(user);//序列化
        unSerializableUser_test("ser.txt");
        System.out.println(unSerializableUser_test("ser.txt"));

    }
    public static void SerializableUser_test(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
        oos.writeObject(obj);
        //向ser.txt写入对象的序列化数据
    }
    public static Object unSerializableUser_test(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream oIs = new ObjectInputStream(new FileInputStream(Filename));
        Object o = oIs.readObject();
        return o;
}
}


由上可以知道


readObject是反序列化的关键

inputObject是序列化关键

代码审计的时候重点找这个就行


安全问题:

入口类readObject直接调用危险方法(最简单)

package org.example.xiaodi_javasec.day04;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class User implements Serializable {
    public String name = "rootreapers";
    public String gender = "no_gay";//不是gay
    public int age = 20;

    public User() {
        System.out.println("无参构造方法被调用");
    }

    public User(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        System.out.println("有参构造方法被调用");
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                '}';
    }
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        Runtime runtime = Runtime.getRuntime();
        runtime.exec("calc");
    }
}



这是需要反序列化的类,注意最后面,重写(准确来说是重载)了readObject


查看序列化以及反序列化函数

package org.example.xiaodi_javasec.day04;

import java.io.*;

public class SerializableUser {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User user = new User();
        SerializableUser_test(user);//序列化
        unSerializableUser_test("ser.txt");
        System.out.println(unSerializableUser_test("ser.txt"));

    }
    public static void SerializableUser_test(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
        oos.writeObject(obj);
        //向ser.txt写入对象的序列化数据
    }
    public static Object unSerializableUser_test(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream oIs = new ObjectInputStream(new FileInputStream(Filename));
        Object o = oIs.readObject();
        return o;
}
}

oIs的readObject方法被调用

oIs指向什么?被读取的序列化数据

然后通过readObject方法将其读取为对象

并且返回


序列化的数据来自User类

而User类现在重载了readObject

序列化后,序列化数据里面包含了重载的readObject

反序列化时调用readObject,实际上调用的是User类里面重载后的

readObject



========以下回复来自于Deepseek(D师傅)========

package org.example.xiaodi_javasec.day04;

import java.io.*;

public class SerializableUser {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User user = new User();
        SerializableUser_test(user);//序列化
        //unSerializableUser_test("ser.txt");
        System.out.println(unSerializableUser_test("ser.txt"));

    }
    public static void SerializableUser_test(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
        oos.writeObject(obj);
        //向ser.txt写入对象的序列化数据
    }
    public static Object unSerializableUser_test(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream oIs = new ObjectInputStream(new FileInputStream(Filename));
        Object o = oIs.readObject();
        return o;
}
}




package org.example.xiaodi_javasec.day04;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class User implements Serializable {
    public String name = "rootreapers";
    public String gender = "no_gay";//不是gay
    public int age = 20;

    public User() {
        System.out.println("无参构造方法被调用");
    }

    public User(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        System.out.println("有参构造方法被调用");
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                '}';
    }
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        Runtime runtime = Runtime.getRuntime();
        runtime.exec("calc");
    }
}


为什么第一段代码里面的 Object o = oIs.readObject();
调用的是无参的方法,但是第二段代码里重构的却被执行了
========上面是我的问题======



这是一个关于Java反序列化机制的好问题。你的观察很敏锐,这里涉及到Java反序列化的特殊机制。

核心原因:Java反序列化机制
在Java中,当调用 ObjectInputStream.readObject() 时:

不会调用类的构造方法(无论是无参还是有参)

会调用类的 readObject(ObjectInputStream) 方法(如果类中定义了)

详细解释:
1. 反序列化的过程
java
Object o = oIs.readObject();  // 这个readObject()是ObjectInputStream的方法
这个 readObject() 方法内部会:

读取对象的类信息

寻找类中是否有自定义的 readObject(ObjectInputStream) 方法

如果有,就调用它(这就是为什么你的重构方法被执行了)

如果没有,就使用默认的反序列化逻辑

2. 为什么看起来像是"调用无参方法"?
你的代码执行流程是这样的:

java
public class SerializableUser {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User user = new User();  // 这里调用了无参构造方法(输出"无参构造方法被调用")
        SerializableUser_test(user);  // 序列化
        
        // 下面这行才是反序列化
        System.out.println(unSerializableUser_test("ser.txt"));
    }
}
输出顺序:

text
无参构造方法被调用  // 来自 main 方法中的 new User()
calc.exe 被弹出    // 来自反序列化时调用的自定义 readObject() 方法
User{name='rootreapers', gender='no_gay', age=20}  // toString() 输出
3. 验证反序列化不调用构造方法
你可以修改代码来验证:

java
public class User implements Serializable {
    public User() {
        System.out.println("无参构造方法被调用");
    }
    
    public User(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        System.out.println("有参构造方法被调用");
    }
    
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("自定义的 readObject 方法被调用");
        // 先调用默认的反序列化
        ois.defaultReadObject();
        // 然后执行恶意代码
        Runtime.getRuntime().exec("calc");
    }
}
运行后你会看到输出:

text
无参构造方法被调用  // 序列化前的 new User()
自定义的 readObject 方法被调用  // 反序列化时
安全启示
这正是Java反序列化漏洞的根源之一:

攻击者可以控制反序列化数据,触发自定义的 readObject() 方法

无需调用构造方法,绕过了很多安全检测

即使构造方法中有安全检查,也无济于事

总结
操作	是否调用构造方法	是否调用自定义readObject
new User()	✓	✗
反序列化	✗	✓(如果定义了)
所以,Object o = oIs.readObject() 实际上调用了你定义的 private void readObject(ObjectInputStream ois) 方法,而不是构造方法。这是Java反序列化机制的设计特点,也是安全漏洞的常见来源。

但是默认的readObject底层不是被final修饰了吗
你说得很对!这是一个很重要的细节,我来详细解释一下这个看似矛盾的现象。

Java ObjectInputStream.readObject() 的机制
1. ObjectInputStream 的 readObject() 方法
确实,ObjectInputStream.readObject() 是 final 的,不能被重写:

java
public final Object readObject() throws IOException, ClassNotFoundException {
    // ...
}
2. 但为什么还能调用自定义的 readObject 呢?
这里的关键在于 Java的序列化机制使用了反射机制。在 readObject() 内部,它:

通过反射查找类中是否有特定签名的 readObject 方法:

java
private void readObject(ObjectInputStream in)
如果找到,就通过反射调用它:

java
// ObjectInputStream 内部的简化逻辑
try {
    Method readObjectMethod = cl.getDeclaredMethod("readObject", ObjectInputStream.class);
    readObjectMethod.setAccessible(true);
    readObjectMethod.invoke(obj, this);  // 通过反射调用自定义的 readObject
} catch (NoSuchMethodException e) {
    // 没有自定义方法,使用默认反序列化
}
3. 示例演示
java
import java.io.*;
import java.lang.reflect.Method;

public class Test {
    public static void main(String[] args) throws Exception {
        User user = new User();
        
        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"));
        oos.writeObject(user);
        oos.close();
        
        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
        Object obj = ois.readObject();  // 这里会触发 User.readObject()
        ois.close();
        
        System.out.println(obj);
    }
}

class User implements Serializable {
    private String name = "test";
    
    // 这个方法是 private 的,但会被 ObjectInputStream 通过反射调用
    private void readObject(ObjectInputStream ois) throws Exception {
        System.out.println("自定义 readObject 被调用!");
        // 注意:我们需要手动调用默认的反序列化
        ois.defaultReadObject();
        // 恶意代码
        Runtime.getRuntime().exec("calc");
    }
    
    @Override
    public String toString() {
        return "User{name='" + name + "'}";
    }
}
4. 为什么需要调用 defaultReadObject()?
当类中定义了自定义的 readObject 方法时:

必须手动调用 ois.defaultReadObject() 来执行默认的反序列化

否则,对象的字段不会被正确反序列化

5. 完整的调用链
java
// 1. 程序调用
ObjectInputStream.readObject()  // final 方法,不能被重写

// 2. ObjectInputStream 内部逻辑
if (类有自定义 readObject 方法) {
    // 通过反射调用自定义方法
    invokeCustomReadObject(obj, this);
} else {
    // 执行默认反序列化逻辑
    defaultReadFields(obj);
}

// 3. 自定义方法中的调用链
private void readObject(ObjectInputStream ois) throws Exception {
    ois.defaultReadObject();  // 调用 ObjectInputStream 的默认实现
    // 然后执行自定义代码
}
安全机制设计
这种设计实际上是一种 "模板方法模式":

ObjectInputStream.readObject() 是模板方法(final)

类的 private void readObject(ObjectInputStream) 是可定制的钩子方法

框架(ObjectInputStream)控制流程,子类可以在特定点插入自己的逻辑

攻击面
这正是反序列化漏洞的根源:

攻击者可以控制反序列化的数据

即使类有自定义的 readObject 方法,ObjectInputStream 依然能通过反射调用它

构造方法不被调用,绕过了对象初始化的安全检查

验证测试
你可以试试这个测试:

java
class TestUser implements Serializable {
    public TestUser() {
        System.out.println("构造方法被调用");
        // 这里的安全检查对反序列化无效!
    }
    
    private void readObject(ObjectInputStream ois) throws Exception {
        System.out.println("自定义 readObject 被调用");
        // 完全覆盖默认行为,构造方法永远不会被调用
        this.name = "hacked";
    }
}
总结:虽然 ObjectInputStream.readObject() 是 final 的,但 Java 序列化框架通过反射机制调用类的自定义 readObject(ObjectInputStream) 方法,这为扩展性提供了可能,但也引入了安全风险。

简单来说,为什么弹计算器

反射干的

明明Object o = oIs.readObject();调用的是无参的readObject

但是由于在User类里重写了,所以优先调用重写后的


那么问题来了

(图中内容为vegetable dog的狂叫)

readObject不是被final修饰了不能重写吗

原则上是的

但是这里对重写的readObject采取的措施并不是停止调用,而是通过反射调用


也就是说,这是可以重写的

通过反射可以绕过final限制进行重写并且调用重写后的方法

那为什么要重写成private void,并且还要有一个参数呢

// ObjectInputStream 内部的简化判断逻辑
private boolean hasReadObjectMethod(Class<?> cl) {
    try {
        // 关键:查找特定签名的方法
        Method m = cl.getDeclaredMethod("readObject", ObjectInputStream.class);
        int mods = m.getModifiers();
        
        // 必须满足以下所有条件:
        // 1. 方法名必须是 "readObject"(完全匹配)
        // 2. 必须是 private 方法
        // 3. 必须返回 void
        // 4. 必须只有一个参数,且类型是 ObjectInputStream
        // 5. 必须抛出 IOException 和 ClassNotFoundException
        return Modifier.isPrivate(mods) && 
               m.getReturnType() == void.class &&
               m.getParameterCount() == 1 &&
               m.getParameterTypes()[0] == ObjectInputStream.class;
    } catch (NoSuchMethodException e) {
        return false;  // 没有找到匹配的方法
    }
}

这里是判断是否重写以及调用的逻辑

getDeclaredMethod(“readObject”, ObjectInputStream.class):

查找方法名为readObject,并且参数类型为ObjectInputStream的


getModifiers:获取方法的权限修饰符

 return Modifier.isPrivate(mods) && 
               m.getReturnType() == void.class &&
               m.getParameterCount() == 1 &&
               m.getParameterTypes()[0] == ObjectInputStream.class;

只有符合以下要求才会判断为True

权限私有

返回类型为void

参数只有一个

参数类型是ObjectInputStream


判断为True,证明readObject被重写

然后通过反射调用重写的readObject



这是第一种反序列化的安全问题



接下来是第二种

tostring隐式调用

在php中也学过

当对象作为字符串输出时会触发tostring

java中也是,假如危险方法写在tostring里

那么当对象被输出时,危险方法也被隐式调用了

package org.example.xiaodi_javasec.day04;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class User implements Serializable {
    public String name = "rootreapers";
    public String gender = "no_gay";//不是gay
    public int age = 20;

    public User() {
        System.out.println("无参构造方法被调用");
    }

    public User(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        System.out.println("有参构造方法被调用");
    }

    @Override
    public String toString() {
        Runtime runtime = Runtime.getRuntime();
        try {
            runtime.exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return "User{" +
                "name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                '}';
    }
}




学到这里,建议去审计一下URLDNS(最简单的一条反序列化链)

今天就写到这里




评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码