java反序列化基础之类加载
Flow

前言

cc链看到cc3,涉及到类加载,之前囫囵吞枣地学了些,还是不够牢固。于是写篇笔记给自己理清楚。参考了很多前辈的文章,感谢,如果有任何不对的,欢迎找我指正🙏

Java类加载机制

借用网上一张图

.java 编译后生成 .class文件保存在本地,如果我们要执行class文件,就需要经过一系列的生命周期和初始化操作,最后目的就是让JVM成功执行。

看这个流程图就知道类的加载就是由java类加载器实现的,作用将类文件进行动态加载到java虚拟机内存中运行。

几个类加载器

BootstrapClassLoader

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

ExtensionsClassLoader

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

AppClassLoader

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

UserDefineClassLoader

除了上面说的三种,用户还可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器。

我们介绍的几个ClassLoader之间有父子关系(不是继承),在Java.lang.ClassLoader里面定义了指向父加载器的常量 parent, 可以通过调用 getParent() 方法获取父加载器。可以看一个代码

1
2
3
4
5
6
7
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
}

打印出来是(这里的null其实就是BootstrapClassLoader,因为是c++实现的,所以无法在java中获取到相应的引用)

1
2
3
sun.misc.Launcher$AppClassLoader@5ce65a89
sun.misc.Launcher$ExtClassLoader@1edf1c96
null

几个ClassLoader关系如下:

所有ClassLoader都继承自java.lang.ClassLoader 这个抽象类, ExtClassLoader 和 AppClassLoader 继承自 URLClassLoaderURLClassLoader既可以加载本地字节码,也可以加载远程字节码。

java.lang.ClassLoader 是所有 ClassLoader 的基石,在这个抽象类中定义了几个比较重要的方法

  • loadClass(): 基于双亲委派机制查找 Class, 调用父加载器的 loadClass 方法或自身的 findClass 方法
  • findClass(): 根据名称和位置读取字节码, 并调用 defineClass 方法, 具体实现由子类重写
  • defineClass(): 把 byte 数组形式的字节码转换成对应的 Class 对象 (真正加载字节码的地方)
1
loadClass() -> findClass() -> defineClass()

双亲委派机制

先说一下class有两种加载方式

  • 隐式加载:通过 new 实例化类, 或通过 类名.方法名() 调用其静态方法, 或调用其静态属性

  • 显式加载:通过反射的形式, 例如 Class.forName() 或者调用 ClassLoader 的 loadClass 方法

然后我们的类加载基于双亲委派机制:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载

可以看一下实现代码,java.lang.ClassLoader的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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name); // 检查类是否已经被加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false); // 调用父类的loadClass,委托下一层
} else {
c = findBootstrapClassOrNull(name); // 此时父加载器为 BootstrapClassLoader
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); // 尝试调用自己的 findClass 方法来加载 class

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

简单流程就是1、先检查类是否已经被加载过 2、若没有加载则调用父加载器的loadClass()方法进行加载 3、若父加载器为空则默认使用启动类加载器作为父加载器。 4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

不同场景下代码块加载顺序

基础认知

  • 静态代码块:static{}
  • 构造代码块:{}
  • 无参构造器:ClassName()
  • 有参构造器:ClassName(String name)

先写一个Dog.java

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
package com;

public class Dog {
public static int staticVar;
public int instanceVar;

static {
System.out.println("静态代码块");
}

{
System.out.println("构造代码块");
}

Dog(){
System.out.println("无参构造器");
}
Dog(int instanceVar){
System.out.println("有参构造器");
}

public static void staticAction(){
System.out.println("静态方法");
}
}
场景一:实例化对象
1
2
3
4
5
6
7
package com;

public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
}
}

打印

1
2
3
静态代码块
构造代码块
无参构造器

所以当我们使用new来实例化对象的时候,会先调用静态代码块,再调用动态代码块,再根据不同的实例化方法调用不同的构造器

场景二:调用静态方法
1
2
3
4
5
public class Main {
public static void main(String[] args) {
Dog.staticAction();
}
}

打印

1
2
静态代码块
静态方法

所以当我们不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法

场景三:对类中的静态成员赋值
1
2
3
4
5
public class Main {
public static void main(String[] args) {
Dog.staticVar = 1;
}
}

打印

1
静态代码块

所以对静态成员变量赋值前,会调用静态代码块

场景四:使用class获取类
1
2
3
4
5
public class Main {
public static void main(String[] args) {
Class c = Dog.class;
}
}

没有输出任何东西

场景五:使用forName获取类
1
2
3
4
5
public class Main {
public static void main(String[] args) throws ClassNotFoundException{
Class.forName("com.Dog");
}
}

打印

1
静态代码块

还有

1
2
3
4
Class.forName("com.Dog", true, ClassLoader.getSystemClassLoader());
// 打印“静态代码块”
Class.forName("com.Dog", false, ClassLoader.getSystemClassLoader());
// 没有打印 因为这个通过false控制只加载,没有初始化

利用URLClassLoader加载class文件

URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释 URLClassLoader 的工作过程实际上就是在解释默认的 Java 类加载器的工作流程。

先新建一个calc类,注意这里没有声明Calc再哪个包,就是自由的

1
2
3
4
5
6
7
8
9
10
11
import java.io.IOException;

public class Calc {
static {
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (IOException e){
e.printStackTrace();
}
}
}

构建生成Calc.class文件,把它放在目录下

file协议
1
2
3
4
5
6
7
8
9
10
11
12
13
package com;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class URLLoader {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///Users/lingtian/Downloads/")});
Class calc = urlClassLoader.loadClass("Calc");
calc.newInstance();
}
}
http协议

在Clac.class所在目录开一个http服务

1
2
3
4
5
6
7
8
9
10
11
12
13
package com;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class URLLoader {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://localhost:8000/")});
Class calc = urlClassLoader.loadClass("Calc");
calc.newInstance();
}
}

服务器这边也可以看到接收到请求

jar+file
1
jar -cvf Calc.jar Clac.class

生成一个jar文件

1
2
3
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///Users/lingtian/Downloads/Calc.jar!/")});
Class calc = urlClassLoader.loadClass("Calc");
calc.newInstance();

就是调用前面多一个jar:,总体逻辑是一样的,要注意包声明的位置,需要对得上

jar+http
1
2
3
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:http://127.0.0.1:8000/Calc.jar!/")});
Class calc = urlClassLoader.loadClass("Calc");
calc.newInstance();

利用 ClassLoader#defineClass 直接加载字节码

默认的ClassLoader#defineClass方法是native,逻辑在JVM里面的C语言

看一下defineclass的调用模式,name就是类名,b为字节码数组,off为偏移量,len为字节码长度

1
protected final Class<?> defineClass(String name, byte[] b, int off, int len)

因为是保护的属性,无法在外部直接访问,所以我们需要反射调用defineClass() 方法进行字节码的加载

1
2
3
4
5
6
7
8
9
10
public class URLLoaderTest {
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader(); // 获取系统的类加载器
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);// 通过反射获取到define方法
method.setAccessible(true); // 设置访问权限
byte[] code = Files.readAllBytes(Paths.get("/Users/lingtian/Downloads/Calc.class")); // 获取字节码数组
Class c = (Class) method.invoke(classLoader, "Calc", code, 0, code.length); // 按照defineclass的调用要求依次传入参数。然后加载出类给c,其实也就是Calc类
c.newInstance(); // 实现实例化
}
}

使用ClassLoader#defineClass直接加载字节码有个优点就是不需要出网也可以加载字节码。

利用Unsafe 加载字节码

Unsafe也有defineClass(),本质和上一个差不多

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
byte[] code = Files.readAllBytes(Paths.get("/Users/lingtian/Downloads/Calc.class"));
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);
Class cc = (Class) defineClassMethod.invoke(classUnsafe, "Calc", code, 0, code.length, classLoader, null);
cc.newInstance();
}

TemplatesImpl加载字节码

为什么要用这个

为什么用这个:在实际场景中,因为defineClass方法作用域却是不开放的,所以我们很很难直接利用到它,但是我们的TemplatesImpl里的作用域是default

目标,调用到里面的TemplatesImpl有一个内部类,TransletClassLoader,里面调用了defineClass方法(重写),而且这个内部类继承了ClassLoader,而且这个重写的defineClass方法没有写明作用域,就默认是default,所以可以被类外部调用

调用链

确定了目标,接下来确定一下调用链

1
2
3
4
5
TransletClassLoader#defineClass()
-> TemplatesImpl#defineTransletClasses()
-> TemplatesImpl#getTransletInstance()
-> TemplatesImpl#newTransformer()
-> TemplatesImpl#getOutputProperties()

倒推前面一两个属性都是private,直到newTransformer和getOutputProperties才是public

注意点1

这里有一个点,就是在找defineTransletClasses上一个调用方法的时候,其实是有三个

但是这里getTransletClasses()和 getTransletIndex() 都在调用defineTransletClasses()后没别的动作,那么最后加载的类就无法被实例化/初始化

咱们getTransletInstance()在取得了_class后会对数组里面对应的队形进行无参数初始化,满足了我们的要求

poc

确定了调用链,先构造一个准备被调用的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com;

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 TemplatesBytes() throws IOException{ // 构造方法,每次加载类的时候会自动调用
super();
System.out.println("Hello TemplatesBytes");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}

这里这个类需要继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

然后因为AbstractTranslet这个类里面有一些抽象方法,所以需要重写,然后就直接写我们要做的事情,这里控制打印一下就好

POC:

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
package com;

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("/Users/lingtian/Downloads/TemplatesBytes.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.getOutputProperties();
}
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这个好高明,直接定义一个方法调用,避免冗余,主要逻辑就是获取成员变量然后修改里面的值就好

注意点2

链子一开始可以是newTransformer()也可以是getOutputProperties(),因为都是public,多一个getOutputProperties去调用也没什么影响,直接newTransformer也是可以的,不用纠结这个,直接修改代码里面的这个就行

参考

b站:白日梦组长

https://drun1baby.top/2022/06/03/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-05-%E7%B1%BB%E7%9A%84%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD/

https://exp10it.io/2022/11/java-classloader/

https://xz.aliyun.com/news/8556

https://www.anquanke.com/post/id/260902

https://www.cnblogs.com/hollischuang/p/14260801.html

由 Hexo 驱动 & 主题 Keep