ClassLoader 0x00 ByteCode Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储 在.class文件中。字节码的本质就是一个字节数组 ,它有特定的复杂的内部格式,Java类初始化的时候会调用java.lang.ClassLoader 加载字节码,.class文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的.class文件,并创建对应的class对象,将class文件加载到虚拟机的内存,而在JVM中类的查找与装载就是由ClassLoader 完成的。
ByteCode2Class脚本: https://github.com/hengyunabc/dumpclass
0x01 显式与隐式加载 Java类加载方式分为显式和隐式
显式:利用反射来加载一个类,Class.forName()等。
隐式:通过ClassLoader来动态加载,new 一个类或者 类名.方法名返回一个类。
1 2 3 4 5 6 7 8 9 10 11 @Test public void loadClassTest () throws Exception { Class<?> aClass = Class.forName("java.lang.Runtime" ); System.out.println(aClass.getName()); Class<?> aClass1 = ClassLoader.getSystemClassLoader().loadClass("java.lang.ProcessBuilder" ); System.out.println(aClass1.getName()); }
0x02 ClassLoader ClassLoader(类加载器)主要作用就是将class文件读入内存,并为之生成对应的java.lang.Class对象。
ClassLoader中的一些核心方法有:
loadClass(加载指定的Java类)
findClass(查找指定的Java类)
findLoadedClass(查找JVM已经加载过的类)
defineClass(定义一个Java类)
resolveClass(链接指定的Java类)
当我们正常加载一个Class的时候,方法的执行顺序也如上所示。
内置ClassLoader JVM中存在3个内置ClassLoader:
BootstrapClassLoader 启动类加载器 负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.*、java.io.*、java.nio.*、java.lang.* 等等。
ExtensionClassLoader 扩展类加载器 负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中
AppClassLoader 系统类加载器 才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的 。
自定义ClassLoader 除了内置的这三个ClassLoader,我们还可以自定义ClassLoader ,自定义ClassLoader必须继承java.lang.ClassLoader类。一个简单的demo为例,定义一个类并获取其字节码再利用自定义ClassLoader加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Person { public String id; private int age; public Person (String id,int age) { this .id=id; this .age=age; } public void hello () { System.out.println("func hello called." ); } }import java.net.URI;import java.nio.file.Files;import java.nio.file.Paths;import java.util.Base64;public class Util { public static String getByteArray (String className) throws Exception{ URI uri = Util.class.getClassLoader().getResource(className+".class" ).toURI(); byte [] bytes = Files.readAllBytes(Paths.get(uri)); String base64 = Base64.getEncoder().encodeToString(bytes); return base64; } }
编写自定义ClassLoader,并重写findClass()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import java.lang.reflect.Method;public class MyClassLoader extends ClassLoader { private static String className = "Person" ; private static byte [] byteArray; static { try { byteArray = Util.getByteArray(className); } catch (Exception e) { e.printStackTrace(); } } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { if (name.equals(className)){ return defineClass(className, byteArray, 0 , byteArray.length ); } return super .findClass(name); } public static void main (String[] args) throws Exception{ MyClassLoader myClassLoader = new MyClassLoader (); Class<?> aClass = myClassLoader.loadClass(className); Object o = aClass.newInstance(); Method m = o.getClass().getMethod("hello" ); m.invoke(o); } }
双亲委派模型 双亲委派模型展示了类的实际加载过程以及ClassLoader的使用顺序。
先不考虑自定义ClassLoader,加入我们编写一个普通类,那么它的加载过程就可以用上图来表示。
双亲委派可以简单理解为:向上委派,向下加载
当一个.class文件要被加载时首先会在AppClassLoader检查是否已被加载,如果加载过就不再加载,如果没有被加载则向上,也就是父加载器委派,父加载器重复这个过程直到BootstrapClassLoader,如果BootstrapClassLoader也没有加载过此类则开始向下加载;首先询问BootstrapClassLoader是否可以加载,如果可以就自己加载,如果不可以则向下加载;子加载器重复这个过程直到AppClassloader,如果AppClassloader也无法加载则抛出ClassNotFoundException异常。
如果我们自定义了ClassLoader并加载类,那么就会先从自定义ClassLoader开始加载,顺序上先于AppClassLoader。
补充 从双亲委派模型来理解上文中几个方法(loadClass、findClass….):
如果我们要自定义加载一个类,那么首先调用loadClass去检查这个类是否被加载过,同样是向上委派的过程。如果没有发现类被记载则调用findClass。
findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass 。
defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类。
值得注意的是,defineClass方法并不会调用类的静态代码块或者构造方法,相关的调用如下:
初始化:静态代码块
实例化:构造代码块\无参构造函数
而使用Class.forName进行动态类加载有两种模式,初始化与不初始化,默认是初始化 。
指定初始化会调用静态代码块,禁止初始化不会调用静态代码块。
1 2 3 4 5 Class.forName("Person" ); Class.forName("Person" ,false ,getSystemClassLoader());
0x03 加载字节码的几种方式 1.利用defineClass() 上面已经提到defineClass()会将Class转换成类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM" ); Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello" , code, 0 , code.length); hello.newInstance(); } }
2.利用URLClassLoader 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 来寻找类
注意第三种情况,如果我们使用的是http协议,那么就会使用到Loader来寻找类,也就是说URLClassLoader允许远程加载类 。
1 2 3 4 5 6 7 8 9 10 11 12 import java.net.URL; import java.net.URLClassLoader; public class HelloClassLoader { public static void main ( String[] args ) throws Exception { URL[] urls = {new URL ("http://localhost:8000/" )}; URLClassLoader loader = URLClassLoader.newInstance(urls); Class c = loader.loadClass("Hello" ); c.newInstance(); } }
3.利用xlan TemplatesImpl 是com.sun.org.apache.xalan
包中一个非常有利用价值的类,它是默认包含在JDK中的。
通过调用其newTransformer()
方法我们最终可以调用defineClass()
,具体的细节并不复杂不再赘述,大致过程是
1 newTransformer() -> getTransletInstance() -> defineTransletClasses() -> defineClass()
当然有几个属性要满足条件才能打通利用链:
_name
不为空
_bytecodes
存放字节码
_tfactory
必须是TransformerFactoryImpl类
加载的类必须是AbstractTranslet的子类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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 Test extends AbstractTranslet { public Test () { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;public class Client { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); Class templatesClass = templates.getClass(); Field namefield = templatesClass.getDeclaredField("_name" ); namefield.setAccessible(true ); namefield.set(templates,"aaa" ); byte [] code = Files.readAllBytes(Paths.get("c:\\tmp\\classes\\Test.class" )); byte [][] codes = {code}; Field bytecodesfield = templatesClass.getDeclaredField("_bytecodes" ); bytecodesfield.setAccessible(true ); bytecodesfield.set(templates,codes); Field tfactoryfield = templatesClass.getDeclaredField("_tfactory" ); tfactoryfield.setAccessible(true ); tfactoryfield.set(templates, new TransformerFactoryImpl ()); templates.newTransformer(); } }
4.利用BCELClassLoader 注意: BCELclassloader在jdk8u251之后的版本就无法使用
BCEL字节码是字节码的一种,与上文中提到的字节码在本质上并无不同,仅仅是形式上的变化。
BCEL类存储在com.sun.org.apache.bcel.internal.util包中。
BECL ClassLoader也是一种恢复成一个类并在JVM虚拟机中进行加载的字节序列。
BCEL也是在JDK库中,在com.sun.org.apache.bcel.internal.util的包中有一个ClassLoader类,它是一个ClassLoader类,和默认的java.lang包下的ClassLoader类不同,loadClass实现不同而已。
欲让BCELclassloader识别加载BCEL字节码,需要在开头添加 $$BCEL$$
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 evil.Exploit;import java.util.Base64;public class BCELClassLoaderDemo { public static String generateBcelCode1 (Class clazz) throws Exception { JavaClass evilJavaClazz = Repository.lookupClass(clazz); String code = Utility.encode(evilJavaClazz.getBytes(), true ); String bcelCode = "$$BCEL$$" + code; System.out.println("bcelcode=" + bcelCode); return bcelCode; } public static String generateBcelCode2 (String classBase64) throws Exception { byte [] codes = Base64.getDecoder().decode(classBase64); String code = Utility.encode(codes, true ); String bcelCode = "$$BCEL$$" + code; System.out.println("bcelcode=" + bcelCode); return bcelCode; } public static void main (String[] args) { try { ClassLoader bcelClassLoader = new ClassLoader (); String bcelCode = generateBcelCode2("yv66v...(class字节码的base64编码)..." ); bcelClassLoader.loadClass(bcelCode).newInstance(); } catch (Exception e) { e.printStackTrace(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package evil;import java.io.IOException;public class Exploit { static { try { Runtime.getRuntime().exec("calc.exe" ); } catch (IOException e) { e.printStackTrace(); } } }
5.利用unsafe sun.misc.Unsafe
类中有defineClass()
1 2 3 4 5 6 sun.misc.Unsafe public Class<?> defineClass(String s, byte [] bytes, int i, int i1, ClassLoader classLoader, java.security.ProtectionDomain protectionDomain)
我们可以直接调用
1 2 3 4 5 6 7 8 9 10 Constructor constructor = Unsafe.class.getDeclaredConstructor(); constructor.setAccessible(true );Unsafe unsafe = (Unsafe) constructor.newInstance(); unsafe.defineClass(<className>,<classBytes>,<offset=0 >,<length>);
测试用class自行编写。
unsafe的补充: https://javasec.org/javase/Unsafe