之前在CC1和CC6的链子中
一开始都使用了反射调用Runtime.class
这里的.class对象是类被jvm加载时创建的元数据

而jvm需要加载一个类时
会去磁盘上加载.class文件
这里的.class文件是源码编译后的二进制字节码数据

既然是字节码数据那么和反序列化时使用的序列化数据一样
很多不是可见字符,需要借助编码等方式进行直接传输

当然还有远程加载的方法:

URLClassLoader

package org.example;
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:80/")};
URLClassLoader loader = URLClassLoader.newInstance(urls);
Class c = loader.loadClass("Hello");
c.newInstance();
}
}

这段代码会在80端口的本地网站根目录下加载一个Hello.class

加载的过程

加载的过程经历了三个函数:

  • ClassLoader.loadclass

双亲委派模型:
首先检查类是否已加载(通过 findLoadedClass)。
若未加载,优先委派给父类加载器(parent.loadClass),
父类无法加载时,才调用自身的 findClass
这种分层设计保证了核心类库(如 java.lang
由启动类加载器(Bootstrap ClassLoader)加载,
避免重复加载和安全性问题。

但是这个名字我觉得有翻译问题
应该叫父类委派模型
否则的话可能会造成有两个父类链的误解

  • ClassLoader.findclass

一路向上直到委派到目标后
执行目标类自定义的findClass
从文件、网络等获取类的字节码

  • ClassLoader.defineclass

findclass成功读取字节码后,
传给defineclass
将字节码转换为Class对象

利用TemplatesImpl加载字节码

package com.sun.org.apache.xalan.internal.xsltc.trax;

这个包中的TemplatesImpl有一个内部类TransletClassLoader
他重写了加载字节码的defineclass方法:

static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;

TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}

TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}

public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}

/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}

这个重写的操作使得原本被protectdefineClass方法
变成了default(可以理解为包内public
那我们找找包内有没有public方法使用了它
如果有的话我们就可以在外部调用它了
有一条调用链:

TemplatesImpl#getOutputProperties() -->
TemplatesImpl#newTransformer() -->
TemplatesImpl#getTransletInstance() -->
TemplatesImpl#defineTransletClasses() -->
TransletClassLoader#defineClass()

前两行都是public
这里写个DEMO:

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.util.Base64;


public class TemplatesImplExample {
public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("base64");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] { code });
setFieldValue(obj, "_name", "NOT NULL");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
}
public static void setFieldValue(Object obj, String fieldName, Object value)
throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true); // 允许访问私有字段
field.set(obj, value); // 设置字段值
}
}

base64的部分填我们构造的恶意类的字节码的base64:

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;

public class CustomTransletClass extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,SerializationHandler handler) throws TransletException {}
public CustomTransletClass() {
super();
System.out.println("CustomTransletClass loaded");
}
}

这里作为AbstractTranslet的子类,
是为了防止触发TemplatesImpl类加载器的某些错误
后面遇到了再细研究

BCEL ClassLoader 加载字节码

看看源码:

protected Class loadClass(String class_name, boolean resolve) throws ClassNotFoundException {
Class cl = null;

// First try: lookup hash table.
cl = (Class) classes.get(class_name);
if (cl == null) {
// Second try: Load system class using system class loader. You better don't mess around with them.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;

// Third try: Special request?if (class_name.indexOf("$$BCEL$$") >= 0) {
clazz = createClass(class_name);
} else {// Fourth try: Load classes via repository
clazz = repository.loadClass(class_name);
if (clazz != 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 {// Fourth try: Use default class loader
cl = Class.forName(class_name);
}
}

if (resolve) {
resolveClass(cl);
}
}

classes.put(class_name, cl);

return cl;
}

重写了loadclass方法
源码注释中注明了四次加载尝试,简单梳理一下这个过程:

  1. 首先尝试从 classes(哈希表)中查找是否已经加载过这个类。如果找到了就直接返回
  2. 如果缓存中没有找到,方法会进行第二次尝试:加载系统类。它会遍历 ignored_packages 的字符串数组,这个数组里存放着一些包名的前缀("java.", "javax.", "sun.")。如果要加载的类名以这些前缀开头,那么这个类就被认为是系统类,会通过调用另一个类加载器 deferTo 的 loadClass 方法来加载。这样做应该是为了避免自定义的类加载器干扰到系统类的加载
  3. 如果仍然没有加载成功,方法会进行第三次尝试,处理一个特殊请求:若类名以 $$BCEL$$ 开头则调用 createClass() 创建一个类,这个方法将 BCEL 字节码转换成 JavaClass 对象
  4. 不以 $$BCEL$$ 开头则通过一个 repository 加载类
  5. 如果上面两步成功获取到了 Class 对象(无论是创建的还是从 repository 加载的),就会调用 defineClass()
  6. 上述步骤都失败则使用默认类加载器 Class.forName()

为了创建用于被加载的 BCEL 字节码,可以用 BCEL 提供的两个类 Repository Utility

  • Repository.lookup() 可以加载一个类,解析为 JavaClass 对象
  • Utility.encode() 将 JavaClass 对象转换成 BCEL 字节码

简单的demo:

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.Repository;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

class BCELExample {
public static void main(String []args) throws Exception {
JavaClass cls = Repository.lookupClass(CustomClass.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);

new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();
}
}