沉铝汤的破站

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

Java反序列化の初见

0x00 前言


本篇包含以下元素:

  • Java的命令执行

  • Java的反射机制

  • Java的反序列化

  • 利用Java反序列化与反射调用进行命令执行的demo

可配和代码食用:

https://github.com/chenlvtang/JavaUnserialization/tree/chenlvtang/Java-Reflection_RCE-Example

https://github.com/chenlvtang/JavaUnserialization/tree/chenlvtang/Java-Serialization-Example

0x01 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");
    }

}

Windows下运行这段代码,如果成功弹出计算器,恭喜你,成功在Java中实现了命令执行。(当然,我们除了Runtime还有ProcessImpl可以执行命令,但是这对于入门者来说,可以日后专门再来讨论与对比)

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

很明显就是根据名称获取到某个类,分为Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。

第一个参数为类名;第二个参数为是否初始化,默认为True,在类初始化时,会执行类中的静态代码;第三个参数指定类加载器(负责将其他类加载到JVM中的东东),默认为当前类。使用方法二时,缺省的参数当然就是会设置成默认值啦。

[这个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 接口。该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明以transient注明。),并且它的序列化格式也非常得不同。下面是一个简单的序列化和反序列化的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

成功执行了命令!Java中反序列化漏洞指的就是,在反序列化时,传入重写了readObject并能触发危险操作的构造链(由多个类依赖组成,连锁反应后,具有危害性),造成危害。