先澄清概念:
输入流:从硬盘等存储介质输入到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)
暂无评论