前言
概念
序列化是将Java对象转换为字节序列的过程,以便可以将其写入到持久性存储器或通过网络进行传输。
反序列化是将字节序列转换回Java对象的过程。
实现
与PHP提供了serialize
和unserialize
关键字不同,java并没有这种API
java中的序列化和反序列化是需要开发人员自己写出整个过程
JDK类库提供了序列化的API
java.io.ObjectOutputStream
表示对象输出流
其中writeObject(Object obj)
方法可以将给定参数的obj对象进行序列化,将转换的一连串的字节序列写到指定的目标输出流中。
java.io.ObjectInputStream
该类表示对象输入流
其中readObject(Object obj)
方法会从源输入流中读取字节序列,并将它反序列化为一个java对象并返回。
注:要实现序列化的类对象必须实现了Serializable类或Externalizable类才能被序列化,否则会抛出异常。
大量的库都会重写readObject方法
三种实现方法
以student类为例
若student类实现了serializable接口,则可以通过
objectOutputstream
和objectinputstream
默认的序列化和反序列化方式,对非transient的实例变量进行序列化和反序列化若student类实现了serializable接口,并且定义了
writeObject(objectOutputStream out)
和readObject(objectinputStream in)
方法,则可以直接调用student类的两种方法进行序列化和反序列化若student类实现了Externalizable接口,则必须实现
readExternal(Objectinput in)
和writeExternal(Objectoutput out)
方法进行序列化和反序列化
步骤(serializable接口)
序列化:
创建一个输出流对象,如文件输出流
ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream("Student.txt"));
通过输出流对象的
writeObject()
方法写对象out.writeObject("hello world");
反序列化:
创建文件输入流对象
ObjectInputStream in = new ObjectInputStream(new fileInputStream("Student.txt"));
调用
readObject()
方法String obj1 = (String)in.readObject(); String obj2 = (String)in.readObject();
演示
/** 要序列化和反序列化的类 **/
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public Person(){
}
// 构造函数
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{" +
"name='" + name + "\'" +
", age=" + age +
'}';
}
}
序列化:
/** 序列化 **/
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
// 开发者需要自己写的序列化方法
public class SerializationTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
Person person = new Person("0w0",24);
System.out.println(person);
serialize(person);
}
}
反序列化:
/** 反序列化 **/
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
// 开发者需要自己写的反序列化方法
public class UnserializeTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
Person person = (Person)unserialize("test.txt");
System.out.println(person);
}
}
运行序列化的程序,会生成一个test.txt文件,内容不可直接阅读:
同时终端输出
Person{name='0w0', age=24}
现在再运行反序列化程序
终端成功输出
Person{name='0w0', age=24}
这说明两次运行都调用了__toString
,成功构造了序列化和反序列化
体现出什么问题呢?
只要服务端反序列化数据,客户端传递的类就会被执行,给予攻击者执行任意代码的权利
transient关键字
用来修饰成员变量。被 transient 修饰的成员变量不会被序列化,即在将对象进行序列化时,该成员变量的值不会被保存到字节流中
修改:
private transient String name;
private transient int age; //被transient关键字修饰,不参与序列化
运行序列化和反序列化程序后结果为
Person{name='null', age=0}
可以发现返回了null和0这两个对应的默认空值
Externalizable接口
这一部分具体内容直接看参考csdn的文章
只需要知道Externalizable没有serializable的限制,static和transient关键字修饰的属性也能进行序列化
利用条件
- 都继承了Serializeable接口
- 入口类source(重写readObject、参数类型宽泛、jdk自带就更好、常见函数)
- 调用链(gadget chain)
- 执行类 sink(ssrf,rce….)
入口类
入口类一般是Map,Hashmap,HashTable
这些集合类,因为集合类型宽泛(泛型),因此肯定继承了Serializeable
接口
调用链
所谓调用链就是一条完整的命令执行流程,在入口类中的readObject方法中,最好有一些常见的方法,这样不管我们传什么东西进去,他都可以调用这个方法,也加大了进一步探索的可能
调用链中一般会使用很多重名函数,为了实现不同的效果
可以类比PHP反序列化,连接的是从触发位置开始到执行命令的位置结束,在PHP中可能是__desctruct
到eval
执行类
就是最后RCE的地方,这一部分就至关重要了,要在调用链中找到一个可以执行命令的类,也是相对比较困难的
ysoseial
一个集成了java反序列化各种gadget chains(利用链)的工具
下载地址:https://github.com/frohoff/ysoserial
在/src/main/java/ysoserial下找到主要代码
先看序列化部分
Serializer.java
package ysoserial;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.concurrent.Callable;
public class Serializer implements Callable<byte[]> {
private final Object object;
public Serializer(Object object) {
this.object = object;
}
public byte[] call() throws Exception {
return serialize(object);
}
public static byte[] serialize(final Object obj) throws IOException {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
serialize(obj, out);
return out.toByteArray();
}
public static void serialize(final Object obj, final OutputStream out) throws IOException {
final ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
}
}
这个序列化操作和之前提到的基本是一样的,将一个对象以字节流的形式输出并保存,并触发它的writeObject
再看反序列化部分
Deserializer.java
package ysoserial;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.util.concurrent.Callable;
public class Deserializer implements Callable<Object> {
private final byte[] bytes;
public Deserializer(byte[] bytes) { this.bytes = bytes; }
public Object call() throws Exception {
return deserialize(bytes);
}
public static Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
final ByteArrayInputStream in = new ByteArrayInputStream(serialized);
return deserialize(in);
}
public static Object deserialize(final InputStream in) throws ClassNotFoundException, IOException {
final ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}
public static void main(String[] args) throws ClassNotFoundException, IOException {
final InputStream in = args.length == 0 ? System.in : new FileInputStream(new File(args[0]));
Object object = deserialize(in);
}
}
将一个字节流读入还原为对象并触发它的readObject
然后来到/src/main/java/ysoserial/payloads/util
PayloadRunner.java
package ysoserial.payloads.util;
import java.util.concurrent.Callable;
import ysoserial.Deserializer;
import ysoserial.Serializer;
import static ysoserial.Deserializer.deserialize;
import static ysoserial.Serializer.serialize;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.ObjectPayload.Utils;
import ysoserial.secmgr.ExecCheckingSecurityManager;
/*
* utility class for running exploits locally from command line
*/
@SuppressWarnings("unused")
public class PayloadRunner {
public static void run(final Class<? extends ObjectPayload<?>> clazz, final String[] args) throws Exception {
// ensure payload generation doesn't throw an exception
byte[] serialized = new ExecCheckingSecurityManager().callWrapped(new Callable<byte[]>(){
public byte[] call() throws Exception {
final String command = args.length > 0 && args[0] != null ? args[0] : getDefaultTestCmd();
System.out.println("generating payload object(s) for command: '" + command + "'");
ObjectPayload<?> payload = clazz.newInstance();
final Object objBefore = payload.getObject(command);
System.out.println("serializing payload");
byte[] ser = Serializer.serialize(objBefore);
Utils.releasePayload(payload, objBefore);
return ser;
}});
try {
System.out.println("deserializing payload");
final Object objAfter = Deserializer.deserialize(serialized);
} catch (Exception e) {
e.printStackTrace();
}
}
private static String getDefaultTestCmd() {
return getFirstExistingFile(
"C:\\Windows\\System32\\calc.exe",
"/Applications/Calculator.app/Contents/MacOS/Calculator",
"/usr/bin/gnome-calculator",
"/usr/bin/kcalc"
);
}
private static String getFirstExistingFile(String ... files) {
return "calc.exe";
// for (String path : files) {
// if (new File(path).exists()) {
// return path;
// }
// }
// throw new UnsupportedOperationException("no known test executable");
}
}
可以看到在Payloadrunner中,先将对象序列化再反序列化,其实就是用来运行我们的链,并生成相应的payload
而ysoserial的使用也很简单,大部分gadget的参数都是一条命令
java -jar ysoserial-all.jar CommonsCollections1 "id"
这样子就会生成cc1链的poc,这里执行的命令是id