某json反序列化RCE核心-四个关键点分析
2020-01-20 10:41:27 Author: xz.aliyun.com(查看原文) 阅读量:264 收藏

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[[email protected]]
 */
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);
        }
    }
}

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


文章来源: http://xz.aliyun.com/t/7107
如有侵权请联系:admin#unsafe.sh