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);
}
}
运行结果:
发现通过字节来看序列化内容可读性太低了,可以把序列化内容写入文件中:
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);
}
}
对象序列化:
和字符串序列化差不多
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);
}
}
结果:
依然可以写入文件:
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);
}
}
注意点:
- 实现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");//多执行的命令
}
运行:
那可能会好奇为什么这里重写了如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
TC_OBJECT:标记后面的数据为Object对象
TC_CLASSDESC:类描述符标识,表示一个类中的所有信息
调试分析
在 Object obj=objin.readObject();
处下个断点,一路跟到 ObjectInputStream#readObject
方法,在其中调用了 readObject0
跟进,在此函数中,根据了tc值来进行switch,此时的tc值为TC_OBJECT,也就是0x73十进制数115
然后在 case TC_OBJECT
中,调用了 readOrdinaryObject
,
跟进readOrdinaryObject
中,发现调用了readClassDesc
方法,并把值赋给了desc
继续跟进 readClassDesc
,此方法用来分发处理字节流中 TC_CLASSDESC
的方法,用switch来选择需要处理的方法,这里tc的值就是 TC_CLASSDESC
的值0x72,转成10进制就是114,然后进入switch判断后转到 case TC_CLASSDESC:
跟进到 readNonProxyDesc
方法中,调用了resolveClass
再 resolveClass
中,通过 Class.forName
来实例化了 people
对象,然后返回对象。
回到 readNonProxyDesc
方法中,时cl值变成了 people 对象,然后把cl对象传入了 initNonProxy
,并且赋值给了desc,
然后返回 desc 赋值给了descriptor
最后回到 readOrdinaryObject
方法中,此时的 desc 已经是 people 对象了。
总结下上面的流程顺序为:
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
此处的 people 对象接口类型是Serializable,所以进入了 readSerialData
方法,
最后 readSerialData
方法中用了反射进行调用反序列化对象的 readObject
方法
到readOrdinaryObject
,接下来就是调用readResolve
方法的地方了
用if进行判断,为true则用反射调用反序列化对象的 readResolve
方法,这里没有重写此方法所以为 false,
最后回到 readObject0
,
到这里就是反序列化的底层逻辑了,所以需要继承 Serializable
接口,然后会反射调用反序列化对象的 readobject
方法。