沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

Java反序列化の初见

0x00 前言


本篇包含以下元素:

  • java的反序列化
  • java的反射机制
  • java的命令执行
  • 一个简单的Java反序列化demo

0x01 java的命令执行


之前写过PHP和NodeJs的命令执行,但是一直没接触过Java的命令执行,下面是一个Java执行系统命令的例子:

package com.edu;

public class ExecDemo {
    public static void  main(String[] args) throws Exception{
//        Runtime.getRuntime().exec("calc.exe");//打开一个记事本
        Runtime run  = Runtime.getRuntime();
        run.exec("calc.exe");
    }

}

运行这段代码,将打开Win10下的计算器。

0x01 Java中的反射机制


虽然我现在都不是很清楚为什么要有一个这个机制(可以说自己的高级编程语言基础是很烂的了),以及这个反射机制的实际运用场景(目前只知道它好像连私有属性的方法和成员都能调用),但是我还是大概知道了它的使用方法。下面是利用反射机制来调用上文中的命令执行的例子。

package com.edu;
import java.lang.reflect.Method;

public class ReflectExecDemo {
    public static void main(String[] args) throws Exception{
        //equal: Object run = Runtime.getRuntime();
        Object myRuntime = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
        //得到方法exec, 其参数为String类型
        Method exec = Class.forName("java.lang.Runtime").getMethod("exec",String.class);
        //equal: myruntime.exec("calc.exe");
        exec.invoke(myRuntime, "calc.exe");
    }
}

Java的一个好处就是看名字就能猜出个大概作用,所以下面就大概说一下以上用到的方法(其他方法以后遇到再说)的作用和小细节。

Class.forName

很明显就是获取到这个类,貌似也没什么要注意的

getMethod

获取方法,有两个参数,第二个参数可以缺省: getMethod(String name, Class<?>...parameterTypes),第一个参数代表的是方法的名字,第二个参数代表的是该方法参数的类型(三个点表示可变参数列表,接收0或多个参数),如果该方法没有参数,则getMethod的第二个参数可以缺省或者为传入null。

invoke

顾名思义,即“调用”,也有两个参数:invoke(Object obj, Object ... args)。要与getMethod结合使用,一般是先用前者获取到一个Method类,再用invoke调用,举个上文的例子应该就便于理解invoke方法的参数意义了:

Method exec = Class.forName("java.lang.Runtime").getMethod("exec",String.class);
//equal: myruntime.exec("calc.exe");
exec.invoke(myRuntime, "calc.exe");

可以看到,invoke的第一个参数即为调用该方法的(如上例的exec方法)类,第二个参数为传入该方法的参数值,同样也是一个可变参数列表。

0x02 java的反序列化


Java中的反序列化和PHP不同的是,在Java中一个类要能够被反序列化,则其必须实现了java.io.Serializable接口(该类必须实现java.io.Serializable 接口。该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。),并且它的序列化格式也非常得不同。下面是一个简单的序列化和反序列化的demo:

1.要被反序列化的类Chenlvtang.class

package com.edu;
import java.io.Serializable;

public class Chenlvtang implements Serializable{
    private String name;
    private String level;
    public Chenlvtang(){
        this.level = "noob";
        this.name = "chenlvtang";
    }
}

2.进行序列化和反序列化操作的SerializeDemo.class

package com.edu;
import java.io.*;

public class SerializDemo{
    public static void main(String[] args) throws Exception{
        Chenlvtang chenlvtang = new Chenlvtang();
        
        //开始序列化
        FileOutputStream file = new FileOutputStream("chenlvtang.bin");
        ObjectOutputStream ser = new ObjectOutputStream(file);
        ser.writeObject(chenlvtang);
        ser.close();

        //开始反序列化
        FileInputStream file1 = new FileInputStream("chenlvtang.bin");
        ObjectInputStream unser = new ObjectInputStream(file1);
        Object result = unser.readObject();
        unser.close();
        System.out.println(result);
    }
}

3.序列化文件

image-20210510225501084

其中ACED是魔术头,0005是流协议版本,其他的就暂且不深入了解。

0x03 反序列化漏洞demo


可以看到上文中进行反序列操作的时候用了到了readObject方法,并且是拿序列化内容进行调用,如果我们传入一个类,其中重写了readObject方法,那么在反序列化的时候是否会调用呢?下面重写chenlvtang.class, 并在里面重写readObject方法。

package com.edu;
import java.io.Serializable;

public class Chenlvtang implements Serializable{
    private String name;
    private String level;

    public Chenlvtang(){
        this.level = "noob";
        this.name = "chenlvtang";
    }

    private void readObject(java.io.ObjectInputStream stream) throws Exception {
        stream.defaultReadObject();//调用默认的方法
        System.out.println("hihi");
    }
}

运行结果:

image-20210510231059139

可以看到,成功的调用了。那我们现在是不是可以想一下把其中的hihi换成上文中危险的命令执行呢?下面我们再次重写readObject方法

private void readObject(java.io.ObjectInputStream stream) throws Exception {
    stream.defaultReadObject();//调用默认的方法
    Runtime.getRuntime().exec("calc.exe");
}

运行结果:

image-20210510231746601

成功执行了命令!