目录

java反序列化

序列化反序列化基础

在Java语言中,实现序列化与反序列化的类:

位置: Java.io.ObjectOutputStream   java.io.ObjectInputStream

序列化:  ObjectOutputStream类 –> writeObject() 注:该方法对参数指定的obj对象进行序列化 ,把字节序列写到一个目标输出流中,按Java的标准约定是给文件一个.ser扩展名

反序列化: ObjectInputStream类 –> readObject() 注:该方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

java在序列化一个对象时,会调用writeObject方法,参数类型时ObjectOutputStream,开发者可以将任何内容写入这个Stream中;反序列化时会调用readObject,可以从中读取到前面写入的内容,并进行处理。

字符串序列化:

import java.io.*;

public class Main implements Serializable {

//定义序列化的方法吗,将对象转化为字节流
   public static byte[] serialize(final Object obj) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream(); //创建一个字节数组输出流
        ObjectOutputStream objOut = new ObjectOutputStream(out); //创建对象输出流
        objOut.writeObject(obj);  //将传入对象进行序列化
        System.out.println(out.toString()); //打印序列化内容
        return out.toByteArray(); //转换为字节数组
        //所以序列化一开始就是创建输出流,可以是字节文件、字节数组或者字节流,然后再创建对象输出流,最后调用writeObject进行序列化,还可以将对象流转换为字节流进行返回。
    }

//定义反序列化的方法,将字节流转化为对象
    public static Object deserialize(byte[] serialized) throws IOException, ClassNotFoundException {
        ByteArrayInputStream in = new ByteArrayInputStream(serialized);//将字节数组转化为字节输入流
        ObjectInputStream objIn = new ObjectInputStream(in);//在将字节输入流初始化为对象输入流
        return objIn.readObject(); //返回反序列化对象
        //反序列化就是将字节流先变为字节输入流,然后再是对象输入流,最后进行反序列化。
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String s = "hello";
        byte[] ObjectBytes=serialize(s);
        String after = (String) deserialize(ObjectBytes); //把对象转换为字符串
        System.out.println(after);
    }
}

运行结果:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240603212209.png

发现通过字节来看序列化内容可读性太低了,可以把序列化内容写入文件中:

import java.io.*;

public class Main implements Serializable {

    //定义序列化的方法吗,将对象转化为字节流
    public static void serialize(final Object obj) throws IOException {
        FileOutputStream files=new FileOutputStream("ser.bin"); //创建一个文件类型对象
        ObjectOutputStream objfiles = new ObjectOutputStream(files); //并将文件类型对象作为序列化时写入的位置
        objfiles.writeObject(obj);  //将传入对象进行序列化
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String s = "hello";
        serialize(s);
    }
}

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240603212642.png

对象序列化:

和字符串序列化差不多

import java.io.*;

public class Main implements Serializable {

    //定义序列化的方法吗,将对象转化为字节流
    public static byte[] serialize(final Object obj) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream(); //创建一个字节输出流对象
        ObjectOutputStream objOut = new ObjectOutputStream(out); //并将字节输出流对象作为序列化时写入的位置
        objOut.writeObject(obj);  //将传入对象进行序列化
        System.out.println(out.toString());//打印序列化内容
        return out.toByteArray();
    }

    //定义反序列化的方法,将字节流转化为对象
    public static Object deserialize(byte[] serialized) throws IOException, ClassNotFoundException {
        ByteArrayInputStream in = new ByteArrayInputStream(serialized);//将字节数组转化为字节输入流
        ObjectInputStream objIn = new ObjectInputStream(in);//放入ObjectInputStream对象中待序列化
        return objIn.readObject().getClass(); //readObject()就能返回序列化对象,加个getClass()更直观
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        People people = new People("ZhangSan", "boy", 18);//People对象,需要在创个People.java文件
        byte[] ObjectBytes=serialize(people);
        Object after =  deserialize(ObjectBytes); 
        System.out.println(after);
    }
}

结果:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240603213304.png

依然可以写入文件:

import java.io.*;
public class SerializableTest {
    public static void serialize(Object obj) throws Exception{
    ObjectOutputStream files=new ObjectOutputStream(new FileOutputStream("ser2.bin"));
    files.writeObject(obj);

    }

    public static void main(String[] args) throws Exception{
        People people = new People("ZhangSan", "boy", 18);
        serialize(people);

    }
}

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240603213443.png

注意点:

  • 实现Serializable和Externalizable接口的类的对象才能被序列化,最下面有解释。
  • Externalizable 接口继承自 Serializable 接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式。
  • 实现java.io.Serializable接口才可被反序列化,而且所有属性必须是可序列化的(用transient 关键字修饰的属性除外,不参与序列化过程)

对象序列化包括如下步骤:

1、创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2、通过对象输出流的writeObject()方法写对象。

对象反序列化的步骤如下:

1、创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2、通过对象输入流的readObject()方法读取对象。

java反序列化漏洞

原理:

如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。从上面可以看到java序列化内容相比php还是比较难读的,所以很难直接构造恶意命令,但依然可以通过重写readObject来进行漏洞利用。

利用:

我们可以在People类中重写readObject方法。

private void readObject(java.io.ObjectInputStream ois) throws java.io.IOException,ClassNotFoundException{
        ois.defaultReadObject(); //默认进行反序列化
        Runtime.getRuntime().exec("calc.exe");//多执行的命令
}

运行:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240603215856.png

那可能会好奇为什么这里重写了如readObject的方法就会执行这个readObject方法呢,同样在最下面进行了解释。

ysoserial工具

ysoserial是java反序列化漏洞的一个工具。可以直接下载编译好的jar文件,直接就能用。

https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar

或者下载源码自己进行编译,建议这样做因为方便查看 poc 的源码。

反序列化底层分析

参考:https://www.cnblogs.com/yyhuni/p/15127416.html

反序列化的 demo,

package org.example;  
  
import java.io.*;  
  
public class sertest {  
    public static void main(String[] args) throws Exception{  
        people p=new people("gaoren");  
        ser(p);  
        deser("test.bin");  
    }  
    public static class people implements Serializable{  
        private String name;  
        people (String name){  
            this.name=name;  
        }  
    }  
    public static void ser(Object obj)throws Exception{  
        FileOutputStream out=new FileOutputStream("test.bin");  
        ObjectOutputStream objout=new ObjectOutputStream(out);  
        objout.writeObject(obj);  
    }  
    public static void deser(String filename)throws Exception{  
        FileInputStream in =new FileInputStream(filename);  
        ObjectInputStream objin=new ObjectInputStream(in);  
        Object obj=objin.readObject();  
    }  
}

然后利用 SerializationDumper 查看生成的字节流,

java -jar SerializationDumper-v1.13.jar -r test.bin

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213202035424.png

TC_OBJECT:标记后面的数据为Object对象

TC_CLASSDESC:类描述符标识,表示一个类中的所有信息

调试分析

Object obj=objin.readObject(); 处下个断点,一路跟到 ObjectInputStream#readObject 方法,在其中调用了 readObject0

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213203309060.png

跟进,在此函数中,根据了tc值来进行switch,此时的tc值为TC_OBJECT,也就是0x73十进制数115

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213203638499.png

然后在 case TC_OBJECT 中,调用了 readOrdinaryObject

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213203711913.png

跟进readOrdinaryObject中,发现调用了readClassDesc方法,并把值赋给了desc

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213203800446.png

继续跟进 readClassDesc,此方法用来分发处理字节流中 TC_CLASSDESC 的方法,用switch来选择需要处理的方法,这里tc的值就是 TC_CLASSDESC 的值0x72,转成10进制就是114,然后进入switch判断后转到 case TC_CLASSDESC:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213203930007.png

跟进到 readNonProxyDesc 方法中,调用了resolveClass

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213204045807.png

resolveClass 中,通过 Class.forName 来实例化了 people 对象,然后返回对象。

回到 readNonProxyDesc 方法中,时cl值变成了 people 对象,然后把cl对象传入了 initNonProxy,并且赋值给了desc,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213204747156.png

然后返回 desc 赋值给了descriptor

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213204919941.png

最后回到 readOrdinaryObject 方法中,此时的 desc 已经是 people 对象了。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213205009754.png

总结下上面的流程顺序为:

readOrdinaryObject -》 readClassDesc -》 readNonProxyDesc -》 resolveClass,

  • readClassDesc:分发用于处理字节流中TC_CLASSDESC的方法,用switch来选择需要处理的方法
  • readNonProxyDesc:真正用来处理字节流中的TC_CLASSDESC方法,会调用resolveClass进行创建反序列化的对象
  • resolveClass:是用Class.forName来创建ObjectA对象的地方,在这里可以做一个检查校验,用于反序列化拦截。weblogic中补丁拦截就是在此进行的

然后是 readOrdinaryObject,这里的 readOrdinaryObject 就是真正操作调用序列化类中,readObject、readResolve、readExternal方法的地方,

接着上面debug,拿到了desc的值后往下走,做了一个判断 desc.isExternalizable,如果序列化的接口是Externalizable类型,就进入readExternalData,否则进入readSerialData

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213210742254.png

此处的 people 对象接口类型是Serializable,所以进入了 readSerialData 方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213210921295.png

最后 readSerialData 方法中用了反射进行调用反序列化对象的 readObject 方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213211014054.png

readOrdinaryObject,接下来就是调用readResolve方法的地方了

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213211152625.png

用if进行判断,为true则用反射调用反序列化对象的 readResolve 方法,这里没有重写此方法所以为 false,

最后回到 readObject0

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250213211337484.png

到这里就是反序列化的底层逻辑了,所以需要继承 Serializable 接口,然后会反射调用反序列化对象的 readobject 方法。