目录

  1. 1. 前言
  2. 2. 概念
  3. 3. 实现
    1. 3.1. 三种实现方法
    2. 3.2. 步骤(serializable接口)
    3. 3.3. 演示
  4. 4. transient关键字
  5. 5. Externalizable接口
  6. 6. 利用条件
    1. 6.1. 入口类
    2. 6.2. 调用链
    3. 6.3. 执行类
  7. 7. ysoseial

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Java内置反序列化

2023/7/12 Web Java 反序列化
  |     |   总文章阅读量:

前言

参考csdn的文章

参考先知社区的文章

参考Boogipop的博客


概念

序列化是将Java对象转换为字节序列的过程,以便可以将其写入到持久性存储器或通过网络进行传输。

反序列化是将字节序列转换回Java对象的过程。


实现

与PHP提供了serializeunserialize关键字不同,java并没有这种API

java中的序列化和反序列化是需要开发人员自己写出整个过程

JDK类库提供了序列化的API

java.io.ObjectOutputStream

表示对象输出流

其中writeObject(Object obj)方法可以将给定参数的obj对象进行序列化,将转换的一连串的字节序列写到指定的目标输出流中。

java.io.ObjectInputStream

该类表示对象输入流

其中readObject(Object obj)方法会从源输入流中读取字节序列,并将它反序列化为一个java对象并返回。

注:要实现序列化的类对象必须实现了Serializable类或Externalizable类才能被序列化,否则会抛出异常。

大量的库都会重写readObject方法

三种实现方法

以student类为例

  1. 若student类实现了serializable接口,则可以通过objectOutputstreamobjectinputstream默认的序列化和反序列化方式,对非transient的实例变量进行序列化和反序列化

  2. 若student类实现了serializable接口,并且定义了writeObject(objectOutputStream out)

    readObject(objectinputStream in)方法,则可以直接调用student类的两种方法进行序列化和反序列化

  3. 若student类实现了Externalizable接口,则必须实现readExternal(Objectinput in)writeExternal(Objectoutput out)方法进行序列化和反序列化

步骤(serializable接口)

序列化:

  1. 创建一个输出流对象,如文件输出流

    ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream("Student.txt"));
  2. 通过输出流对象的writeObject()方法写对象

    out.writeObject("hello world");

反序列化:

  1. 创建文件输入流对象

    ObjectInputStream in = new ObjectInputStream(new fileInputStream("Student.txt"));
  2. 调用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文件,内容不可直接阅读:

image-20230713232424233

同时终端输出

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中可能是__desctructeval

执行类

就是最后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