ClassLoader动态类加载

发布者:SecIN
发布于:2023-10-24 10:52

在JNDI、RMI等攻击中,我们常会用到这么一段测试代码

public class Exec {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

将它反编译成class文件后,开启监听服务即可进行远程类加载,但这段代码中没有main等执行方法,它又为啥会执行呢?这其实跟类加载机制有关

类加载机制

类的加载分为以下几步:

wKg0C2UVPdOAFrbMAAAzU6jJTv4421.png

类加载初始化和实例化的时候会执行对应的代码块

  • 初始化:静态代码块(一次执行中静态代码块只能执行一次)
  • 实例化:匿名代码块、构造方法

实例

以Person类为例

public class Person {

    public String name;
    private int age;
    public static int id;

    static {
        System.out.println("静态代码块");
    }
    public  static void staticAction(){
        System.out.println("静态方法");
    }
    {
        System.out.println("匿名代码块");
    }

    public Person() {
        System.out.println("无参构造方法");
    }

    public Person(String name, int age) {
        System.out.println("有参构造方法");
        this.name = name;
        this.age = age;
    }

}

类进行实例化

public static void main(String[] args)  {
    new Person("S1nJa",2);
}

结果:

静态代码块
匿名代码块
有参构造方法

调用静态方法

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

结果:

静态代码块
静态方法

class类加载

用class关键字时,只进行了类加载并没有进行初始化,所以不会调用任何代码块

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

Class.forName

Class.forName()默认会调用forName0,而第二个参数默认为true,所以会进行初始化

wKg0C2UVPd2AVO7QAABCyoLtQdI527.png

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

结果:

静态代码块

若想不进行初始化则可以调用它的其他构造方法

 public static void main(String[] args) throws ClassNotFoundException {    
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        Class.forName("Person",false,cl);
 }

loadClass

默认不进行初始化,需要结合newInstance()

ClassLoader cl = ClassLoader.getSystemClassLoader();
Class c = cl.loadClass("Person");
c.newInstance();

结果:

静态代码块
匿名代码块
无参构造方法

JNDI注入等,都是通过lookup中调用newInstance()实现的,所以会实例化class文件中的恶意类,调用对应的恶意代码块。而这些类的初始化、实例化都是由类加载器完成的,所以看下类加载器。

类加载器

类加载器主要分为两类:

  • JVM 默认类加载器
    主要由 “引导类加载器”、“扩展类加载器”、“系统类加载器” 三方面组成。
  • 用户自定义类加载器
    用户可以编写继承 java.lang.ClassLoader类的自定义类来自定义类加载器。

引导类加载器(BootstrapClassLoader)

底层原生代码是C++语言编写,属于jvm一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载/jre/lib/rt.jar包下的内容

wKg0C2UVPeaAckFAABTy0k1owk080.png

随便找了个/jre/lib/rt.jar包下的类Applet,查看它的付类加载器,由于他是被BootstrapClassLoader加载的,所以并没有父类加载器,结果为null。

wKg0C2UVPe6ADvNBAABJvKWeLE562.png

扩展类加载器(ExtensionsClassLoader)

主要负责加载/jre/lib/ext或者java.ext.dirs中指明目录对的java扩展库

wKg0C2UVPfSAH9oVAACvPBM3lMw979.png

该类位于sun.misc.Launcher\$ExtClassLoader

wKg0C2UVPfyAHIaXAABwz3sKWUQ139.png

系统类加载器(AppClassLoader)

主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器那它就是我们程序中的默认加载器。

ClassLoaderTest本身就是由它来加载的,该类位于sun.misc.Launcher\$AppClassLoader

wKg0C2UVPgKAH86GAABZvlYfK6g748.png

自定义类加载器(CustomClassLoader)

可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器

双亲委派

一般来说,当需要加载一个类时,JVM的三种默认类加载器是按按需加载的方式相互配合使用,这就衍生出了双亲委派模式

wKg0C2UVPhKADX2AABgOhd225s775.png

双亲委派的意思是:当类加载器需要加载一个类时,首先它会把这个类请求委派给父类加载器去完成 (如AppClassLoader会委派给ExtClassLoader,ExtClassLoader会委派给BootStrapClassLoader)。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。这里的双亲其实只是翻译问题,他们之间并不存在继承关系,只是逻辑上的指向。

优点

  1. 避免重复加载:双亲委派模型使得每个类加载器首先向上委派给父类加载器,因此在整个类加载器层次结构中,任何类都只会被加载一次。这避免了同一个类被多个不同的类加载器重复加载,减少了内存占用和冲突问题。
  2. 确保类的安全性:由于父类加载器在子类加载器之前尝试加载类,可以保证系统核心库类的安全性。例如,JDK 中的核心类库(如java.lang.String)由BootstrapClassLoader负责加载,因此无法通过自定义的类加载器来替换或修改这些核心类的行为,防止恶意代码对核心类进行篡改。

例如在创建个Java.lang包,自定义一个StringTest类,在执行StringTest.class时进行了类加载,而这里违背了双亲委派,所以系统不允许加载运行

wKg0C2UVPhiAeFL3AACB8JITAU0054.png

类加载过程

前边用loadClass或forName进行了类加载,但例如加载的Person类都是在程序中写好的,若要进行利用就要考虑能否进行任意类加载

#loadClass
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class c = cl.loadClass("Person");
#forName
Class.forName("Person");

先看下底层原理,主要用到以下几个方法:

  • loadClass(加载指定的Java类)
  • findLoadedClass(查找JVM已经加载过的类)
  • findClass(查找指定的Java类)
  • defineClass(将字节码转换为class对象)
  • resolveClass(链接指定的Java类)

loadClass

调用findLoadedClass()查找是否加载过该类,这里没加载所以c=null,进入if这里就是双亲委派的过程

wKg0C2UVPiGACQbwAACogKRphwU666.png

先判断是否有父类加载器,如果有则调用父类加载器的loadClass进行加载,这里进入ExtClassLoader的loadClass

由于ExtClassLoader中没有loadClass(),所以还是调用到了他的父类ClassLoader的loadClass(),逻辑仍然是判断是有存在父类加载器,由于BootstrapClassLoader是用C写的,所以这里parent为null,通过else里的findBootstrapClassOrNull寻找

wKg0C2UVPieAPGVXAACogKRphwU279.png

Person类并不在rt.jar包中,所以肯定也没有,接着到下边的findClass(),当前调用的是ExtClassLoader的findClass(),person类也不是扩展类所以自然也不会有

wKg0C2UVPi6ALKOUAACmrYwcPfo746.png

回到AppClassLoader的loadClass(),进入URLClassLoader.findClass()方法

wKg0C2UVPjeAFK29AACnAZhfQAY147.png

接着调用到了他的父类SecureClassLoader的defineClass()

protected final Class<?> defineClass(String name,
                                     byte[] b, int off, int len,
                                     CodeSource cs)
{
    return defineClass(name, b, off, len, getProtectionDomain(cs));
}

最后又回到了ClassLoader的defineClass(),

wKg0C2UVPkCAbeSzAADFRfUShXc349.png

里边调用defineClass1,进行了类加载,defineClass1使用C实现的

private native Class<?> defineClass1(String name, byte[] b, int off, int len,
                                     ProtectionDomain pd, String source);

foreName

在调试forName()时发现,它的底层也是调用loadClass()实现的

wKg0C2UVPl6AWGdAAACq66WLLc0011.png

那它为啥会进行初始化呢?主要是在C中通过对initialize的判断,进行初始化设置,可参考:https://zhuanlan.zhihu.com/p/205324628

URLClassLoader

loadClass的调用过程主要涉及到ClassLoader和它的几个子类

ClassLoader—>SecureClassLoader—>URLClassLoader—>AppClassLoader

类的加载就在于findClass()的defineClass()方法

loadClass—>findClass—>defineClass

其中URLClassLoader,可以加载URL中的类请求,那既然这样如果参数可控我们便可以进行任意类加载

先构造恶意类,将其编译成class文件

public class Exec {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

ClassLoaderTest

public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException {
        URLClassLoader cl = new URLClassLoader(new URL[]{new URL("http://localhost:7777/")});
        Class exec = cl.loadClass("Exec");
        exec.newInstance();
    }

}

wKg0C2UVPmuAcZoAAACaAvViJU199.png

除此外也可以用file协议或者jar协议

URLClassLoader cl = new URLClassLoader(new URL[]{new URL("jar:file:///D:\\java\\Security\\Exec.jar!\\")});
Class exec = cl.loadClass("Exec");

URLClassLoader cl = new URLClassLoader(new URL[]{new URL("file:///D:\\java\\Security\\")});
Class exec = cl.loadClass("Exec");

自定义ClassLoader

自定义类加载器步骤:

1、继承ClassLoader类

2、覆盖findClass()方法

3、在findClass()方法中调用defineClass()方法

假设将Exec.class文件进行base64加密,加密后使用java自带的classLoader就不能进行初始化了,那就可以使用自定义的ClassLoader进行加载

package ClassLoader;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class CustomClassLoader extends ClassLoader{
    private String classPath;

    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }
    public byte[] readFile(String path) throws IOException {
        byte[] bytes = Files.readAllBytes(Paths.get(path));
        byte[] decode = Base64.getDecoder().decode(bytes);
        return decode;
    }


    @Override
    protected Class<?> findClass(String name) {
        try {
            String fullPath = classPath+File.separator+name+".class";
            byte[] bytes = readFile(fullPath);
            return defineClass(name,bytes,0,bytes.length);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

使用readFile方法读取文件,并进行base64解密,之后调用defineClass进行加载

public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        CustomClassLoader cl = new CustomClassLoader("D:\\java\\Security");
        Class<?> c = cl.loadClass("Exec");
        c.newInstance();
    }
}

wKg0C2UVPnOARPXeAACppik3Oxg386.png



声明:该文观点仅代表作者本人,转载请注明来自看雪