某json反序列化RCE核心-四个关键点分析

2020-01-20 约 3283 字 预计阅读 16 分钟

声明:本文 【某json反序列化RCE核心-四个关键点分析】 由作者 threedr3am 于 2020-01-20 09:41:27 首发 先知社区 曾经 浏览数 190 次

感谢 threedr3am 的辛苦付出!

0x01 前言

A fast JSON parser/generator for Java.一个Java语言实现的JSON快速解析/生成器。

官方描述:

Fastjson is a Java library that can be used to convert Java Objects into their JSON representation. 
It can also be used to convert a JSON string to an equivalent Java object. 
Fastjson can work with arbitrary Java objects including pre-existing objects that you do not have source-code of.

Fastjson Goals:
- Provide best performance in server side and android client
- Provide simple toJSONString() and parseObject() methods to convert Java objects to JSON and vice-versa
- Allow pre-existing unmodifiable objects to be converted to and from JSON
- Extensive support of Java Generics
- Allow custom representations for objects
- Support arbitrarily complex objects (with deep inheritance hierarchies and extensive use of generic types)

Fastjson是阿里巴巴开源的Apache顶级项目,在国内开发圈子中被使用广泛,由于它假定有序的解析特性,其相对于Jackson,性能会有一定的优势,不过个人觉得,相对于磁盘、网络IO等时间损耗,这样的提升对于大部分企业来讲,意义并不大。

因为Fastjson在国内被广泛使用,也就是说受众广,影响范围大,那么一但出现安全漏洞,被不法分子利用,将会对企业、用户造成极大损失。对于我们研究安全的人员来讲,研究分析Fastjson的源码,跟踪Fastjson安全漏洞,可以更好的挖掘出潜在的安全隐患,提前消灭它。

我曾经从网络上看到过很多对Fastjson分析的文章,但大部分都是对于新漏洞gadget chain触发的源码debug跟踪,缺少对于一些关键点代码的分析描述,也就是说,我看完之后,该不懂还是不懂,最后时间花出去了,得到的只是一个证明可用的exp...因此,我这篇文章,将针对Fastjson反序列化部分涉及到的关键点代码进行详细的讲解,其中一共四个关键点“词法解析、构造方法选择、缓存绕过、反射调用”,希望大家看完之后,将能完全搞懂Fastjson漏洞触发的一些条件以及原理。

0x02 四个关键点

  • 词法解析
  • 构造方法选择
  • 缓存绕过
  • 反射调用

1、词法解析

词法解析是Fastjson反序列化中比较重要的一环,一个json的格式、内容是否能被Fastjson理解,它充当了最重要的角色。

在调用JSON.parse(text)对json文本进行解析时,将使用缺省的默认配置

public static Object parse(String text) {
    return parse(text, DEFAULT_PARSER_FEATURE);
}

DEFAULT_PARSER_FEATURE是一个缺省默认的feature配置,具体每个feature的作用,我这边就不做讲解了,跟这一小节中的词法解析关联不大

public static int DEFAULT_PARSER_FEATURE;
static {
    int features = 0;
    features |= Feature.AutoCloseSource.getMask();
    features |= Feature.InternFieldNames.getMask();
    features |= Feature.UseBigDecimal.getMask();
    features |= Feature.AllowUnQuotedFieldNames.getMask();
    features |= Feature.AllowSingleQuotes.getMask();
    features |= Feature.AllowArbitraryCommas.getMask();
    features |= Feature.SortFeidFastMatch.getMask();
    features |= Feature.IgnoreNotMatch.getMask();
    DEFAULT_PARSER_FEATURE = features;
}

而如果想使用自定义的feature的话,可以自己或运算配置feature

public static Object parse(String text, int features) {
    return parse(text, ParserConfig.getGlobalInstance(), features);
}

而这里,我们也能看到,传入了一个解析配置ParserConfig.getGlobalInstance(),它是一个默认的全局配置,因此,如果我们想要不使用全局解析配置的话,也可以自己构建一个局部的解析配置进行传入,这一系列的重载方法都给我们的使用提供了很大的自由度。

接着,我们可以看到,最终其实都走到这一步,创建DefaultJSONParser类实例,接着对json进行解析

public static Object parse(String text, ParserConfig config, int features) {
    if (text == null) {
        return null;
    }

    DefaultJSONParser parser = new DefaultJSONParser(text, config, features);
    Object value = parser.parse();

    parser.handleResovleTask(value);

    parser.close();

    return value;
}

然后我们跟进DefaultJSONParser构造方法中,可以看到其中调用了另一个重载的构造方法,我们可以重点关注第二个参数,也就是JSONScanner,它就是词法解析的具体实现类了

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

看类注释,可以知道,这个类为了词法解析中的性能提升,做了很多特别的处理

//这个类,为了性能优化做了很多特别处理,一切都是为了性能!!!

/**
 * @author wenshao[szujobs@hotmail.com]
 */
public final class JSONScanner extends JSONLexerBase {
    ...
}

在分析该词法解析类之前,我这里列出一些该类以及父类中变量的含义,有助于后续的代码分析:

text:json文本数据
len:json文本数据长度
token:代表解析到的这一段数据的类型
ch:当前读取到的字符
bp:当前字符索引
sbuf:正在解析段的数据,char数组
sp:sbuf最后一个数据的索引
hasSpecial=false:需要初始化或者扩容sbuf

可以从JSONScanner构造方法看到,text、len、bp、ch的大概意义,并且对utf-8 bom进行跳过

public JSONScanner(String input, int features){
    super(features);

    text = input;//json文本数据
    len = text.length();//json文本数据长度
    bp = -1;//当前字符索引

    next();
    if (ch == 65279) { // utf-8 bom
        next();
    }
}

接着在构造方法中,会调用next进行对text中一个一个字符的获取,可以看到bp值初始值为-1,在第一次调用时执行++bp变为0,即开始读取第一个字符的索引

public final char next() {
    int index = ++bp;
    return ch = (index >= this.len ? //
            EOI //
            : text.charAt(index));
}

再跟进DefaultJSONParser主要构造方法,其中lexer是词法解析器,这里我们跟踪得到其实现为JSONScanner,也就是我们前面所讲的那个,而input就是需要解析的json字符串,config为解析配置,最重要的就是symbolTable,我称之为符号表,它可以根据传入的字符,进而解析知道你想要读取的一段字符串

public DefaultJSONParser(final Object input, final JSONLexer lexer, final ParserConfig config){
    this.lexer = lexer;//JSONScanner
    this.input = input;//需要解析的json字符串
    this.config = config;//解析配置
    this.symbolTable = config.symbolTable;

    //获取当前解析到的字符
    int ch = lexer.getCurrent();
    if (ch == '{') {
        lexer.next();
        ((JSONLexerBase) lexer).token = JSONToken.LBRACE;
    } else if (ch == '[') {
        lexer.next();
        ((JSONLexerBase) lexer).token = JSONToken.LBRACKET;
    } else {
        lexer.nextToken(); // prime the pump
    }
}

从上面的if、else流判断中可以知道,当开始头解析时,如果能解析到'{'或'[',就会赋值token,指明当前读到的token类型(在Fastjson中,会对json数据字符串一位一位的提取,然后比对,得出当前位置的词法类型,也即token),接着继续执行next()滑动到下一个字符。如果不能解析到'{'或'['开头,就会执行nextToken(),后续parse也会继续执行nextToken()

nextToken,顾名思义就是下一个token,其中实现逻辑会对字符一个一个的进行一定的解析,判断出下一个token类型

而整个Fastjson反序列化时,就是这样根据不断的next()提取出字符,然后判断当前token类型,接着根据token类型的不同,会有不一样的处理逻辑,表现为根据token类型做一定的数据字符串读取,并根据读取出来的字符串数据,进行反序列化成Java Object

我们回到nextToken中来:

public final void nextToken() {
    sp = 0;

    for (;;) {
        pos = bp;

        if (ch == '/') {
            skipComment();
            continue;
        }

        if (ch == '"') {
            scanString();
            return;
        }

        if (ch == ',') {
            next();
            token = COMMA;
            return;
        }

        if (ch >= '0' && ch <= '9') {
            scanNumber();
            return;
        }

        if (ch == '-') {
            scanNumber();
            return;
        }

        switch (ch) {
            case '\'':
                if (!isEnabled(Feature.AllowSingleQuotes)) {
                    throw new JSONException("Feature.AllowSingleQuotes is false");
                }
                scanStringSingleQuote();
                return;
            case ' ':
            case '\t':
            case '\b':
            case '\f':
            case '\n':
            case '\r':
                next();
                break;
            case 't': // true
                scanTrue();
                return;
            case 'f': // false
                scanFalse();
                return;
            case 'n': // new,null
                scanNullOrNew();
                return;
            case 'T':
            case 'N': // NULL
            case 'S':
            case 'u': // undefined
                scanIdent();
                return;
            case '(':
                next();
                token = LPAREN;
                return;
            case ')':
                next();
                token = RPAREN;
                return;
            case '[':
                next();
                token = LBRACKET;
                return;
            case ']':
                next();
                token = RBRACKET;
                return;
            case '{':
                next();
                token = LBRACE;
                return;
            case '}':
                next();
                token = RBRACE;
                return;
            case ':':
                next();
                token = COLON;
                return;
            case ';':
                next();
                token = SEMI;
                return;
            case '.':
                next();
                token = DOT;
                return;
            case '+':
                next();
                scanNumber();
                return;
            case 'x':
                scanHex();
                return;
            default:
                if (isEOF()) { // JLS
                    if (token == EOF) {
                        throw new JSONException("EOF error");
                    }

                    token = EOF;
                    eofPos = pos = bp;
                } else {
                    if (ch <= 31 || ch == 127) {
                        next();
                        break;
                    }

                    lexError("illegal.char", String.valueOf((int) ch));
                    next();
                }

                return;
        }
    }

}

可以看到,就是前面所说的,根据当前读到的字符,而选择执行不同的字符串提取逻辑,我们这小节最核心的代码就位于scanString(),当判断当前字符为双引号时,则执行这个方法,我们看一下具体实现

public final void scanString() {
    np = bp;
    hasSpecial = false;
    char ch;
    for (;;) {
        ch = next();

        if (ch == '\"') {
            break;
        }

        if (ch == EOI) {
            if (!isEOF()) {
                putChar((char) EOI);
                continue;
            }
            throw new JSONException("unclosed string : " + ch);
        }

        if (ch == '\\') {
            if (!hasSpecial) {
                ...扩容
            }

            ch = next();

            switch (ch) {
                case '0':
                    putChar('\0');
                    break;
                case '1':
                    putChar('\1');
                    break;
                case '2':
                    putChar('\2');
                    break;
                case '3':
                    putChar('\3');
                    break;
                case '4':
                    putChar('\4');
                    break;
                case '5':
                    putChar('\5');
                    break;
                case '6':
                    putChar('\6');
                    break;
                case '7':
                    putChar('\7');
                    break;
                case 'b': // 8
                    putChar('\b');
                    break;
                case 't': // 9
                    putChar('\t');
                    break;
                case 'n': // 10
                    putChar('\n');
                    break;
                case 'v': // 11
                    putChar('\u000B');
                    break;
                case 'f': // 12
                case 'F':
                    putChar('\f');
                    break;
                case 'r': // 13
                    putChar('\r');
                    break;
                case '"': // 34
                    putChar('"');
                    break;
                case '\'': // 39
                    putChar('\'');
                    break;
                case '/': // 47
                    putChar('/');
                    break;
                case '\\': // 92
                    putChar('\\');
                    break;
                case 'x':
                    char x1 = next();
                    char x2 = next();

                    boolean hex1 = (x1 >= '0' && x1 <= '9')
                            || (x1 >= 'a' && x1 <= 'f')
                            || (x1 >= 'A' && x1 <= 'F');
                    boolean hex2 = (x2 >= '0' && x2 <= '9')
                            || (x2 >= 'a' && x2 <= 'f')
                            || (x2 >= 'A' && x2 <= 'F');
                    if (!hex1 || !hex2) {
                        throw new JSONException("invalid escape character \\x" + x1 + x2);
                    }

                    char x_char = (char) (digits[x1] * 16 + digits[x2]);
                    putChar(x_char);
                    break;
                case 'u':
                    char u1 = next();
                    char u2 = next();
                    char u3 = next();
                    char u4 = next();
                    int val = Integer.parseInt(new String(new char[] { u1, u2, u3, u4 }), 16);
                    putChar((char) val);
                    break;
                default:
                    this.ch = ch;
                    throw new JSONException("unclosed string : " + ch);
            }
            continue;
        }

        if (!hasSpecial) {
            sp++;
            continue;
        }

        if (sp == sbuf.length) {
            putChar(ch);
        } else {
            sbuf[sp++] = ch;
        }
    }

    token = JSONToken.LITERAL_STRING;
    this.ch = next();
}

从代码中可以看到,当下一个字符遇到也是双引号时,就会结束scanString()循环,因为Fastjson认为读取到的字符串为空字符串,接着就是EOI的判断,不过我们这边关心的是'\\'的处理,因为在java中,只要是硬编码的字符串,对于一些转义字符,都需要使用'\'对其转义,那么'\\'其实正真就代表则字符'\'

接着看后面的switch-case处理,可以看出,其实就是对json数据字符串中'\0 \1 \2 \3 \4 \5 \6 \7 \b \t \n \u000B \f \F \r \" \\ \x \u'等双字节字符的处理,总结一下:

例:
\0 \1 \2 \3 \4 \5 \6 \7 \b \t \n \r \" \' \/ \\\ 
等,java字符串读入之后会变成两个字符,因此,fastjson会把它转换会单个字符
\f \F双字符都会转成单字符\f
\v双字符转成\u000B单字符
\x..四字符16进制数读取转成单字符
\u....六字符16进制数读取转成单字符

其实,对于\x和\u的词法处理,才是反序列化RCE中的核心,也就是我这一节词法解析中最想要讲的内容,我曾经遇到某道CTF题目,它在程序的filter层,对json数据的@type进行的过滤处理,而唯一能绕过它的办法,正恰恰是词法解析中对\x和\u也即16进制、Unicode的处理,通过对字符的十六进制转换或者Unicode处理,我们就可以通过以下的方式进行filter过滤器的绕过,而对于开发人员来说,也可以通过针对这种绕过方式进行filter的加强:

例:

@\u0074ype     ->     @type
@\x74ype       ->     @type

接着就是执行DefaultJSONParser.parse(),根据上一步中token的识别,进行解析处理

public Object parse(Object fieldName) {
    final JSONLexer lexer = this.lexer;
    switch (lexer.token()) {
        case SET:
            ...HashSet集合的处理
        case TREE_SET:
            ...TreeSet集合的处理
        case LBRACKET:
            ...读取到"[",数组的处理
        case LBRACE:
            ...读取到"{",对象解析的处理
        case LITERAL_INT:
            ...
        case LITERAL_FLOAT:
            ...
    }
}

对象解析,反序列化的利用流程,基本都是走到LBRACE或LBRACKET中,进入对象的解析,而对象解析中,基本都会利用到符号表进行数据的提取:

public final Object parseObject(final Map object, Object fieldName) {
    final JSONLexer lexer = this.lexer;

    if (lexer.token() == JSONToken.NULL) {
        lexer.nextToken();
        return null;
    }

    if (lexer.token() == JSONToken.RBRACE) {
        lexer.nextToken();
        return object;
    }

    if (lexer.token() == JSONToken.LITERAL_STRING && lexer.stringVal().length() == 0) {
        lexer.nextToken();
        return object;
    }

    if (lexer.token() != JSONToken.LBRACE && lexer.token() != JSONToken.COMMA) {
        throw new JSONException("syntax error, expect {, actual " + lexer.tokenName() + ", " + lexer.info());
    }

   ParseContext context = this.context;
    try {
        boolean isJsonObjectMap = object instanceof JSONObject;
        Map map = isJsonObjectMap ? ((JSONObject) object).getInnerMap() : object;

        boolean setContextFlag = false;
        for (;;) {
            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;
            //判断到双引号开端的,利用符号表读取双引号闭合之间字符串,从而提取出key
            if (ch == '"') {
                key = lexer.scanSymbol(symbolTable, '"');
                lexer.skipWhitespace();
                ch = lexer.getCurrent();
                if (ch != ':') {
                    throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
                }
            } else if (ch == '}') {
                ...
            }
            ...

            //判断到key为@type,则进行checkAutoType,然后反序列化成Java Object
            if (key == JSON.DEFAULT_TYPE_KEY
                    && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
                ...

                clazz = config.checkAutoType(typeName, null, lexer.getFeatures());

                ...


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

        }
    } finally {
        this.setContext(context);
    }
}

最后,总结一下:在反序列化RCE中,我们可以利用词法解析中\x\u的十六进制或者Unicode的处理,进行绕过一些检查机制。

2、构造方法选择

构造方法的选择,我这一小节中,主要想讲解的是,在Fastjson反序列化中,针对每个class的特点,到底Fastjson会选择class的哪个构造方法进行反射实例化,到底是否可以不存在无参构造方法。

在上一节:

clazz = config.checkAutoType(typeName, null, lexer.getFeatures());

...

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

在通过config.checkAutoType后会返回一个class,接着会根据class选择一个ObjectDeserializer,做Java Object的反序列化

而对于ObjectDeserializer的选择,很多class返回的都是一些没有利用价值的ObjectDeserializer:

deserializer
├─ASMDeserializerFactory.java
├─AbstractDateDeserializer.java
├─ArrayListTypeFieldDeserializer.java
├─AutowiredObjectDeserializer.java
├─ContextObjectDeserializer.java
├─DefaultFieldDeserializer.java
├─EnumDeserializer.java
├─ExtraProcessable.java
├─ExtraProcessor.java
├─ExtraTypeProvider.java
├─FieldDeserializer.java
├─FieldTypeResolver.java
├─JSONPDeserializer.java
├─JavaBeanDeserializer.java
├─JavaObjectDeserializer.java
├─Jdk8DateCodec.java
├─MapDeserializer.java
├─NumberDeserializer.java
├─ObjectDeserializer.java
├─OptionalCodec.java
├─ParseProcess.java
├─PropertyProcessable.java
├─PropertyProcessableDeserializer.java
├─ResolveFieldDeserializer.java
├─SqlDateDeserializer.java
├─StackTraceElementDeserializer.java
├─ThrowableDeserializer.java
└TimeDeserializer.java

以及一些根据JSONType注解等不太会存在安全漏洞的条件处理,而对于大部分可利用gadget chains的处理,最终都会走到

com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.reflect.Type)
->
com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)
->
com.alibaba.fastjson.parser.ParserConfig#createJavaBeanDeserializer

接着在其中,构建了JavaBeanInfo,在build方法中,会构建一个JavaBeanInfo对象,其中存储了选择哪个构造方法、字段信息、反射调用哪个方法等等,用于在最后的反射实例化时,做相应的处理

JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz
        , type
        , propertyNamingStrategy
        ,false
        , TypeUtils.compatibleWithJavaBean
        , jacksonCompatible
);

跟进JavaBeanInfo.build

JSONType jsonType = TypeUtils.getAnnotation(clazz,JSONType.class);
if (jsonType != null) {
    PropertyNamingStrategy jsonTypeNaming = jsonType.naming();
    if (jsonTypeNaming != null && jsonTypeNaming != PropertyNamingStrategy.CamelCase) {
        propertyNamingStrategy = jsonTypeNaming;
    }
}

可以看到,一开始就会从class中取JSONType注解,根据注解配置去选择参数命名方式,默认是驼峰式

接着会取出class的字段、方法、构造方法等数据,并且判断出class非kotlin实现时,如果构造方法只有一个,则调用getDefaultConstructor获取默认的构造方法

Class<?> builderClass = getBuilderClass(clazz, jsonType);

Field[] declaredFields = clazz.getDeclaredFields();
Method[] methods = clazz.getMethods();
Map<TypeVariable, Type> genericInfo = buildGenericInfo(clazz);

boolean kotlin = TypeUtils.isKotlin(clazz);
Constructor[] constructors = clazz.getDeclaredConstructors();

Constructor<?> defaultConstructor = null;
if ((!kotlin) || constructors.length == 1) {
    if (builderClass == null) {
        defaultConstructor = getDefaultConstructor(clazz, constructors);
    } else {
        defaultConstructor = getDefaultConstructor(builderClass, builderClass.getDeclaredConstructors());
    }
}

从getDefaultConstructor的实现中可以清楚的看到,对于这个构造方法,如果它是无参构造方法或一参(自身类型)构造方法,则就会作为默认构造方法(反序列化对Java Object实例化时反射调用的构造方法),即defaultConstructor

static Constructor<?> getDefaultConstructor(Class<?> clazz, final Constructor<?>[] constructors) {
    if (Modifier.isAbstract(clazz.getModifiers())) {
        return null;
    }

    Constructor<?> defaultConstructor = null;

    for (Constructor<?> constructor : constructors) {
        if (constructor.getParameterTypes().length == 0) {
            defaultConstructor = constructor;
            break;
        }
    }

    if (defaultConstructor == null) {
        if (clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers())) {
            Class<?>[] types;
            for (Constructor<?> constructor : constructors) {
                if ((types = constructor.getParameterTypes()).length == 1
                        && types[0].equals(clazz.getDeclaringClass())) {
                    defaultConstructor = constructor;
                    break;
                }
            }
        }
    }

    return defaultConstructor;
}

若不存在这样特性的构造方法,则

boolean isInterfaceOrAbstract = clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers());
if ((defaultConstructor == null && builderClass == null) || isInterfaceOrAbstract) {
    ...抽象类或接口类
} else if ((factoryMethod = getFactoryMethod(clazz, methods, jacksonCompatible)) != null) {
    ...使用JSONCreator注解指定构造工厂方法
} else if (!isInterfaceOrAbstract) {
   ...


    for (Constructor constructor : constructors) {
        Class<?>[] parameterTypes = constructor.getParameterTypes();
        ...

        boolean is_public = (constructor.getModifiers() & Modifier.PUBLIC) != 0;
        if (!is_public) {
            continue;
        }
        String[] lookupParameterNames = ASMUtils.lookupParameterNames(constructor);
        if (lookupParameterNames == null || lookupParameterNames.length == 0) {
            continue;
        }

        if (creatorConstructor != null
                && paramNames != null && lookupParameterNames.length <= paramNames.length) {
            continue;
        }

        paramNames = lookupParameterNames;
        creatorConstructor = constructor;
    }
    ...
}

从上述代码中可以了解到,若非接口类,并且没有使用JSONCreator注解的话,则会对构造方法进行遍历选择,如果是以下三个class的话,会直接作为构造方法creatorConstructor

org.springframework.security.web.authentication.WebAuthenticationDetails
org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
org.springframework.security.core.authority.SimpleGrantedAuthority

而非public的构造方法会被直接跳过。

接着使用com.alibaba.fastjson.util.ASMUtils#lookupParameterNames获取出所有的构造方法参数,若取出的参数为空,则也会直接跳过。

若前面遍历构造方法时,已有creatorConstructor选择,以及asm取出的参数数量<=构造方法参数数量,则也会直接跳过。

也就是说,除非是非公有public的,否则必然会选择一个creatorConstructor。

defaultConstructor:无参和一参(自身类型入参)构造方法

creatorConstructor:非defaultConstructor,遍历取最后的一个构造方法

总结的来讲:若可以找到defaultConstructor,则不再遍历选择creatorConstructor,否则必须遍历查找creatorConstructor。

3、缓存绕过

缓存绕过,这是什么意思?我们上一小节已经详细的描述并总结了构造方法的选择逻辑。其中构造方法的选择分为defaultConstructor和creatorConstructor

我们这一节主要分析的关键点:缓存绕过,位于com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int),它是在json数据反序列化时,通过@type指定class后,对class是否可被反序列化进行检查,其中检查包括黑名单、白名单、构造方法等

我们跟进com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int):

if (typeName == null) {
    return null;
}

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

可以看到,一开始的地方,会对typeName做一定的检查,typeName是我们传入json数据@type这个key对应的值value

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

接着是对一些期望class的判断,若不是期望中反序列化指定的class,后续黑白名单的检查会不管是否启用autoTypeSupport。

String className = typeName.replace('$', '.');
Class<?> clazz = null;

final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;

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

final long h3 = (((((BASIC ^ className.charAt(0))
        * PRIME)
        ^ className.charAt(1))
        * PRIME)
        ^ className.charAt(2))
        * PRIME;

boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES,
        TypeUtils.fnv1a_64(className)
) >= 0;

if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
    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, true);
            if (clazz != null) {
                return clazz;
            }
        }
        if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

再接着,会对className的前两位字符进行判断是否允许,然后会二分查找内部白名单INTERNAL_WHITELIST_HASHCODES,若不在内部白名单内,并且开启了autoTypeSupport或者是预期以外的class,则会对className后面的字符继续进行hash处理后与外部白名单、黑名单进行判断,决定其是否被支持反序列化。

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

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

if (clazz == null) {
    clazz = typeMapping.get(typeName);
}

if (internalWhite) {
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
}

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

从上面的代码中,我们可以看到有好几个if流程从jvm缓存中获取class,也即会对class进行一定的判断,决定是否从缓存map中加载,我们这一节重点关注的其实是TypeUtils.getClassFromMapping:

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

跟进TypeUtils.getClassFromMapping代码实现,可以看到,其具体是从mappings缓存中获取class

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

接着会判断class是否在内部白名单内,若在白名单内,会直接通过检查,返回class

if (internalWhite) {
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
}

跟进TypeUtils.loadClass,可以看到第三个参数true,决定了在其方法实现中是否会对查找出来的class进行缓存到mappings

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);
        if (cache) {
            mappings.put(className, clazz);
        }
        return clazz;
    } catch(Throwable e){
        // skip
    }
    return clazz;
}

下一步,可以看到,又是一段黑白名单的检查代码,不过这次是autoTypeSupport不启用的情况下

if (!autoTypeSupport) {
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
        char c = className.charAt(i);
        hash ^= c;
        hash *= PRIME;

        if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        // white list
        if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
            if (clazz == null) {
                clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
            }

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

            return clazz;
        }
    }
}

接着,在这段代码中通过asm对其class进行visit,取出JsonType注解信息

boolean jsonType = false;
InputStream is = null;
try {
    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();
    }
} catch (Exception e) {
    // skip
} finally {
    IOUtils.close(is);
}

而从后续代码中也可以了解到,若到这一步,class还是null的时候,就会对其是否注解了JsonType、是否期望class、是否开启autotype进行判断。若判断通过,然后会判断是否开启autotype或是否注解了JsonType,从而觉得是否会在加载class后,对其缓存到mappings这个集合中,那也就是说,我只要开启了autoType的话,在这段逻辑就会把class缓存道mappings中

if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
    boolean cacheClass = autoTypeSupport || jsonType;
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}

上面这一块是一个很关键的地方,也是我这一小节缓存绕过的主要核心

最后,也就是我们需要去绕过的地方了,像一般大部分情况下,我们基本不可能找到注解有JsonType的class的gadget chains,所以,这一步中对jsonType判断,然后缓存class到mappings基本就没什么利用价值了。但这块逻辑中,我们需要注意的其实是JavaBeanInfo在build后,对其creatorConstructor的判断

if (clazz != null) {
    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());
        }
    }

    JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
    if (beanInfo.creatorConstructor != null && autoTypeSupport) {
        throw new JSONException("autoType is not support. " + typeName);
    }
}

if (!autoTypeSupport) {
    throw new JSONException("autoType is not support. " + typeName);
}

从creatorConstructor和autoTypeSupport的判断流程中,我们可以得知,只要autoTypeSupport为true,并且creatorConstructor(上一小节就是描述构造方法的选择,这里判断的构造方法是第二种选择)不为空,则会抛出异常,而后面的!autoTypeSupport判断,也表示了,就算上一步通过设置autoTypeSupport为true可以绕过,但是最终也避免不了它抛出异常的制裁:

if (!autoTypeSupport) {
    throw new JSONException("autoType is not support. " + typeName);
}

那怎么办呢?这时候就得看前面的代码了,我前面也说了,在对黑白名单进行一轮检查后的时候,会有这个判断:

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

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

从mappings中直接获取,接着在后面判断道class不为空时,直接就返回了,从而提前结束该方法执行,绕过构造方法creatorConstructor和autoTypeSupport的判断

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

那我们怎么才能从缓存中获取到class呢?答案其实前面也说了:

if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
    boolean cacheClass = autoTypeSupport || jsonType;
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}

对,没错,就是这里,我们只要开启了autoTypeSupport,绕后通过两次反序列化,在第一次反序列化时,虽然最后会抛出异常,但是在抛异常前,做了上述代码中的缓存class到mappings的处理,那么,在第二次反序列化该class的时候,我们就可以顺利的从缓存中取出了,从而绕过后面的判断。

4、反射调用

反射调用,就是fastjson反序列化的最后一个阶段了,当经历了前面:词法解析、构造方法选择、缓存绕过阶段之后,我们离RCE就差最后的一步了,也就是反射调用,从而触发gadget chain的执行,最终实现RCE。

接着,又回到DefaultJSONParser.parseObject来,也就是第2小节构造方法选择部分

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

前面也说了,大部分可利用的gadget chain,config.getDeserializer(clazz)最终都会走到

com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.reflect.Type)
->
com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)
->
com.alibaba.fastjson.parser.ParserConfig#createJavaBeanDeserializer

而反射调用,是选择setter还是getter方法调用,亦或者是直接反射field设值,它需要一系列的判断处理,最终确定下来在JavaBeanDeserializer中执行deserialze时,到底会做什么样的反射调用处理

我们跟进JavaBeanInfo.build,前面一大段,我们在讲构造方法选择的时候已经简单讲过了,但是我们并没讲一个小地方,就是FieldInfo的创建和添加

if (creatorConstructor != null && !isInterfaceOrAbstract) { // 基于标记 JSONCreator 注解的构造方法
    ...
    if (types.length > 0) {
        ...

            FieldInfo fieldInfo = new FieldInfo(fieldName, clazz, fieldClass, fieldType, field,
                    ordinal, serialzeFeatures, parserFeatures);
            add(fieldList, fieldInfo);
        ...
    }

    //return new JavaBeanInfo(clazz, builderClass, null, creatorConstructor, null, null, jsonType, fieldList);
} else if ((factoryMethod = getFactoryMethod(clazz, methods, jacksonCompatible)) != null) {
    ...
        Field field = TypeUtils.getField(clazz, fieldName, declaredFields);
        FieldInfo fieldInfo = new FieldInfo(fieldName, clazz, fieldClass, fieldType, field,
                ordinal, serialzeFeatures, parserFeatures);
        add(fieldList, fieldInfo);

    ...

} else if (!isInterfaceOrAbstract) {

    ...
    if (paramNames != null
            && types.length == paramNames.length) {
        ...
            FieldInfo fieldInfo = new FieldInfo(paramName, clazz, fieldClass, fieldType, field,
                    ordinal, serialzeFeatures, parserFeatures);
            add(fieldList, fieldInfo);
        ...
    }
}

在省略大部分无关代码后,可以看到,对于这三种情况下的处理,最终都是实例化FieldInfo,然后直接调用add添加到集合fieldList中来,但是细心去看FieldInfo重载的构造方法可以发现,它存在多个构造方法,其中就有入参method的构造方法:

public FieldInfo(String name, // 
                     Class<?> declaringClass, // 
                     Class<?> fieldClass, // 
                     Type fieldType, // 
                     Field field, // 
                     int ordinal, // 
                     int serialzeFeatures, // 
                     int parserFeatures)
public FieldInfo(String name, //
                     Method method, //
                     Field field, //
                     Class<?> clazz, //
                     Type type, //
                     int ordinal, //
                     int serialzeFeatures, //
                     int parserFeatures, //
                     JSONField fieldAnnotation, //
                     JSONField methodAnnotation, //
                     String label)
public FieldInfo(String name, //
                     Method method, //
                     Field field, //
                     Class<?> clazz, //
                     Type type, //
                     int ordinal, //
                     int serialzeFeatures, //
                     int parserFeatures, //
                     JSONField fieldAnnotation, //
                     JSONField methodAnnotation, //
                     String label,
                     Map<TypeVariable, Type> genericInfo)

这种构造方法意味着什么?在后面执行JavaBeanDeserializer.deserialze时,会发现,具有method入参的字段,很有可能会触发方法的执行,从而可以触发gadget chain的执行。

接着,后面就是一串惆怅的代码,无非就是根据setter方法名称智能提取出field名字...,其中会对所有的方法进行两次的遍历,我这边简单总结一下:

  • 第一遍
    ```
  • 静态方法跳过
  • 返回值类型不为Void.TYPE和自身class类型的方法跳过
  • 获取JSONField注解,确定字段field名称,然后和方法添加到集合中
  • 没有JSONField则判断方法名长度是否大于4,不大于4则跳过
  • 判断是否set前缀,不是则跳过
  • 根据setter方法名从第四个字符开始确定字段field名称(需把第一个字符转小写),若是boolean类型,则需把字段第一个字符转大写,然后前面拼接is
  • 根据字段名获取到字段Field后,判断是否注解了JSONField,获取JSONField注解,确定字段field名称,然后和方法添加到集合中
  • 根据setter方法确定的字段名添加到集合
    ```

  • 第二遍

1. 判断方法名长度是否大于4,不大于4则跳过
2. 静态方法跳过
3. 判断方法名称是否get前缀,并且第四个字符为大写,不符合则跳过
4. 方法有入参则跳过
5. 方法返回值不是Collection.class、Map.class、AtomicBoolean.class、AtomicInteger.class、AtomicLong.class或其子孙类则跳过
6. 获取方法上的注解JSONField,根据注解取字段名称
7. 根据getter方法名从第四个字符开始确定字段field名称(需把第一个字符转小写),若是boolean类型,则需把字段第一个字符转大写
8. 根据字段名获取到字段Field后,判断是否注解了JSONField,获取JSONField注解,确定字段field是否可以被反序列化,不可被反序列化则跳过
9. 根据字段名获取集合中是否已有FieldInfo,有则跳过
10. 根据getter方法确定的字段名添加到集合

以上就是总结,从这些总结,我们就不难分析,fastjson反序列化时,class到底哪个方法能被触发。

最后,对于这些添加到集合fieldList中的FieldInfo,会在JavaBeanDeserializer.deserialze中被处理

protected <T> T deserialze(DefaultJSONParser parser, // 
                               Type type, // 
                               Object fieldName, // 
                               Object object, //
                               int features, //
                               int[] setFlags) {
    ...

    try {
        Map<String, Object> fieldValues = null;

        if (token == JSONToken.RBRACE) {
            lexer.nextToken(JSONToken.COMMA);
            if (object == null) {
                object = createInstance(parser, type);
            }
            return (T) object;
        }
    ...
    } finally {
        if (childContext != null) {
            childContext.object = object;
        }
        parser.setContext(context);
    }
}

从上述代码可以看到,配对"@type":"..."之后,如果下一个token不为"}",即JSONToken.RBRACE,则获取反序列化器进行反序列化,根据前面扫描Field得到的信息以及json后续的key-value进行反序列化,如果下一个token为"}",则直接反射实例化返回

判断下一个token为"[",即JSONToken.LBRACKET,则进行数组处理

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

调用构造方法

if (beanInfo.creatorConstructor != null) {
    ...
    try {
        if (hasNull && beanInfo.kotlinDefaultConstructor != null) {
            object = beanInfo.kotlinDefaultConstructor.newInstance(new Object[0]);

            for (int i = 0; i < params.length; i++) {
                final Object param = params[i];
                if (param != null && beanInfo.fields != null && i < beanInfo.fields.length) {
                    FieldInfo fieldInfo = beanInfo.fields[i];
                    fieldInfo.set(object, param);
                }
            }
        } else {
            object = beanInfo.creatorConstructor.newInstance(params);
        }
    } catch (Exception e) {
        throw new JSONException("create instance error, " + paramNames + ", "
                                + beanInfo.creatorConstructor.toGenericString(), e);
    }
    ...
}

最后,通过FieldDeserializer对字段进行反序列化处理,其中,会利用到FieldInfo前面构建时,收集到的信息,例如method、getOnly等,进行判断是否调用某些方法

FieldDeserializer fieldDeserializer = getFieldDeserializer(entry.getKey());
if (fieldDeserializer != null) {
    fieldDeserializer.setValue(object, entry.getValue());
}

可以看到,对于method不为空的fieldInfo,若getOnly为false,则直接反射执行method,若getOnly为true,也就是只存在对应字段field的getter,而不存在setter,则会对其method的返回类型进行判断,若符合,才会进行反射执行该method

Method method = fieldInfo.method;
if (method != null) {
    if (fieldInfo.getOnly) {
        if (fieldInfo.fieldClass == AtomicInteger.class) {
            AtomicInteger atomic = (AtomicInteger) method.invoke(object);
            if (atomic != null) {
                atomic.set(((AtomicInteger) value).get());
            }
        } else if (fieldInfo.fieldClass == AtomicLong.class) {
            AtomicLong atomic = (AtomicLong) method.invoke(object);
            if (atomic != null) {
                atomic.set(((AtomicLong) value).get());
            }
        } else if (fieldInfo.fieldClass == AtomicBoolean.class) {
            AtomicBoolean atomic = (AtomicBoolean) method.invoke(object);
            if (atomic != null) {
                atomic.set(((AtomicBoolean) value).get());
            }
        } else if (Map.class.isAssignableFrom(method.getReturnType())) {
            Map map = (Map) method.invoke(object);
            if (map != null) {
                if (map == Collections.emptyMap()
                        || map.getClass().getName().startsWith("java.util.Collections$Unmodifiable")) {
                    // skip
                    return;
                }

                map.putAll((Map) value);
            }
        } else {
            Collection collection = (Collection) method.invoke(object);
            if (collection != null && value != null) {
                if (collection == Collections.emptySet()
                        || collection == Collections.emptyList()
                        || collection.getClass().getName().startsWith("java.util.Collections$Unmodifiable")) {
                    // skip
                    return;
                }

                collection.clear();
                collection.addAll((Collection) value);
            }
        }
    } else {
        method.invoke(object, value);
    }
}

而对于method为空的情况,根本就不可能对method进行反射调用,除了构建实例时选择的构造方法

} else {
    final Field field = fieldInfo.field;

    if (fieldInfo.getOnly) {
        if (fieldInfo.fieldClass == AtomicInteger.class) {
            AtomicInteger atomic = (AtomicInteger) field.get(object);
            if (atomic != null) {
                atomic.set(((AtomicInteger) value).get());
            }
        } else if (fieldInfo.fieldClass == AtomicLong.class) {
            AtomicLong atomic = (AtomicLong) field.get(object);
            if (atomic != null) {
                atomic.set(((AtomicLong) value).get());
            }
        } else if (fieldInfo.fieldClass == AtomicBoolean.class) {
            AtomicBoolean atomic = (AtomicBoolean) field.get(object);
            if (atomic != null) {
                atomic.set(((AtomicBoolean) value).get());
            }
        } else if (Map.class.isAssignableFrom(fieldInfo.fieldClass)) {
            Map map = (Map) field.get(object);
            if (map != null) {
                if (map == Collections.emptyMap()
                        || map.getClass().getName().startsWith("java.util.Collections$Unmodifiable")) {
                    // skip
                    return;
                }
                map.putAll((Map) value);
            }
        } else {
            Collection collection = (Collection) field.get(object);
            if (collection != null && value != null) {
                if (collection == Collections.emptySet()
                        || collection == Collections.emptyList()
                        || collection.getClass().getName().startsWith("java.util.Collections$Unmodifiable")) {
                    // skip
                    return;
                }

                collection.clear();
                collection.addAll((Collection) value);
            }
        }
    } else {
        if (field != null) {
            field.set(object, value);
        }
    }
}

至此,四个关键点得分析就此结束!

关键词:[‘安全技术’, ‘WEB安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now