近期出现fastjson的远程代码执行漏洞,fastjson国内知名度很高的json库,趁这个机会学习下,与大家分享下漏洞原因,fastjson反序列化流程。
远程代码执行漏洞
反序列原理
1 通过入参类型,生成反序列化器。
2 反序列化器调用入参类型的无参构造函数,创建对象。
3 解析序列化字符串,取出Key,Value值,调用相应的setKey(Value)方法。
这个漏洞主要是fastjson的一个 autotype 功能。下面代码是一个简单例子。
public class MyTest4AutoType {
public static String userStr2="{\"@type\":\"com.mogujie.zhaoyun.mytest.User\",\"age\":12,\"birthday\":1493295579866,\"name\":\"myname\"}";
public static void main(String[] args){
User user2 = (User) JSON.parse(userStr2);
System.err.println(user2.getName());
}
}
这面是fastjson的一个黑科技,json字符串里增加一个特殊K,V值。key 固定为@type,value为此json字符串要反序列化的类全路径。这样就能通过入参,控制json字符串反序列化生成的对象。
下面这块就是fastjson(1.2.24)的代码
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
if (clazz == null) {
object.put(JSON.DEFAULT_TYPE_KEY, typeName);
continue;
}
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());typeName就是入参的类的全路径
上面的方法就是获得这个自定义类,安全漏洞就在这里。原因就是,这个方法没做任何类名的安全校验,也没有白名单机制。
通过前面反序列化原理知道,应用会执行自定义类的set方法赋值。loadClass这个方法没做任何过滤,也就是说可以传入任何jvm加载过的类,并执行setKey方法,Key也是传入的,可变的。
剩下的就是寻找符合以下条件的类:
1,JVM加载过的类;
2,有非静态set方法;
3,入参只有一个。
假如寻找到赋值后改变的是类变量的Class,那就能肆意妄为了。
上面的demo稍加改动,就变成下面的了,虽然后抛出异常,但是ClassLoader.setDefaultAssertionStatus仍然后执行。
public class MyTest4AutoType {
public static String userStr2="{\"@type\":\"com.alibaba.fastjson.util.ASMClassLoader\",\"defaultAssertionStatus\":true}";
public static void main(String[] args){
User user2 = (User) JSON.parse(userStr2);
System.err.println(user2.getName());
}
}
1.2.24之后的版本都修复了这个漏洞,如果代码不做任何修改,自定义类的反序列化都会失败。解决方案就是增加白名单配置,例如 ParserConfig.global.addAccept("com.alibaba"),所有以com.alibaba开始的类都能反序列化了。fastjson还有黑名单配置功能,例如 ParserConfig.global.addDeny("javax.swing"),所有javax.swing开始的类都不反序列化。无论黑名单还是白名单,都是全局配置,可以统一控制fastjson。
反序列化解析
反序列化核心方法:com.alibaba.fastjson.JSON#parseObject(java.lang.String, java.lang.reflect.Type, com.alibaba.fastjson.parser.ParserConfig, com.alibaba.fastjson.parser.deserializer.ParseProcess, int, com.alibaba.fastjson.parser.Feature...);我们以这个方法为入口开始观察反序列化过程。
1,DefaultJSONParser parser = new DefaultJSONParser(input, config,featureValues);
DefaultJSONParser 是反序列化的核心类,是外观模式,包括配置(ParserConfig)、反序列化实现(ObjectDeserializer)、词法解析(JSONLexer)等功能组合在一起,依赖关系可以看下上面的类图。
DefaultJSONParser通过json字符串(input),反序列化全局配置(config),反序列化实例特征配置(featureValues)。这个实例创建好后,反序列化的基础就准备好了。
ParserConfig 全局唯一的反序列化配置,包括反序列化黑名单(denyList),反序列化白名单(acceptList),是否启用asm(asmEnable),反序列化实现类缓存集合(deserializers),常见关键字缓存(SymbolTable)。这个对象是单例的,包括两大部分:系统默认和自定义。asmEnable就是系统默认,程序加载时候会判断系统是否符合启动ams条件,并赋值。acceptList是自定义的,可以根据不同的业务系统配置不同的白名单。
2,T value = (T) parser.parseObject(clazz, null);
这行是反序列化的执行方法,反序列化所有的代码都在这里,分析几个关键代码。
2-1,ObjectDeserializer derializer = config.getDeserializer(type);
这个方法是根据入参类型获得反序列化器,每一个基本类型都有一个对应的反序列化器,如NumberDeserializer等,这些都会初始化并储存到deserializers里。自定义类会利用asm或反射生成,并存储到deserializers里,第二次相同的类型反序列化就不会重复创建,直接从deserializers获得。这种设计有两个优势,一是空间换时间,所有反序列化器都缓存起来。二是每一种类型都对应一种反序列化器,代码清晰,优化更有针对性。
2-2,ASMDeserializerFactory.createJavaBeanDeserializer(ParserConfig config, JavaBeanInfo beanInfo);
这个方法就是根据入参类型利用asm生成反序列化器,作者直接将asm核心代码迁移到fastjon里,所以fastjson不需要额外依赖asm库。
使用ASM条件:
- 非Android系统
- 该类及其除Object之外的所有父类为是public的
- 泛型参数非空
- 非asmFactory加载器之外的加载器加载的类
- 非接口类
- 类的setter函数不大于200
- 类有默认构造函数
- 类不能含有仅有getter的filed
- 类不能含有非public的field
- 类不能含有非静态的成员类
- 类本身不是非静态的成员类
3,return (T) value;
整个反序列化流程就结束了,大家有兴趣的可以自己看看,有很多细节值得大家发现。
序列化类图
反序列化核心方法:com.alibaba.fastjson.JSON#toJSONString(java.lang.Object, com.alibaba.fastjson.serializer.SerializeConfig, com.alibaba.fastjson.serializer.SerializeFilter[], java.lang.String, int, com.alibaba.fastjson.serializer.SerializerFeature...);我们以这个方法为入口开始观察序列化过程,这块因为很多和反序列化的处理方法,设计都是一样的,整体简单描述下。
1,SerializeWriter out = new SerializeWriter(null, defaultFeatures, features);
SerializeWriter继承Writer,能够完成高性能的字符串拼接,相比StringBuilder做了适合json字符串拼装的优化。out初始化包括:申请序列化字符串存储空间(buf),这个空间是申请后是不释放的,放在Threadlocal,避免重复申请带来的性能损耗。序列化特征配置features,包括默认的配置和实例自定义配置,如利用符号(')替代符号("),禁止循环引用,是否输出值为null的字段等,详细的可以看下com.alibaba.fastjson.serializer.SerializerFeature。这块fastjson是用按位或计算的。
2,JSONSerializer serializer = new JSONSerializer(out, config);
JSONSerializer是序列化的核心类,是外观模式,包括配置(SerializeConfig)、序列化实现(ObjectSerializer)、序列化输出(SerializeWriter)、过滤器(SerializeFilterable)等功能组合在一起,依赖关系可以看下上面的类图。
SerializeConfig是全局唯一的,和ParserConfig功能结构基本一样。它继承自IdentityHashMap,IdentityHashMap是一个长度默认为1024的Hash桶,每个桶存放相同Hash的Entry(可看做链表节点,包含key、value、next指针、hash值)做成的单向链表,IdentityHashMap实现了HashMap的功能,但能避免HashMap并发时的死循环。
serializers,存储的序列化器,设计和deserializers是一致的。
SerializeFilterable列表,可视为在生成json串的各阶段、各地方定制序列化,大致如下:
- BeforeFilter :序列化时在最前面添加内容
- AfterFilter :序列化时在最后面添加内容
- PropertyFilter :根据PropertyName和PropertyValue来判断是否序列化
- ValueFilter :修改Value
- NameFilter :修改key
- PropertyPreFilter :根据PropertyName判断是否序列化
SerialContex 序列化上下文,在引用或循环引用中使用,该值会放入references的Hash桶(IdentityHashMap)缓存。
3,serializer.write(object);
这行是序列化的执行方法,列出几个关键代码。
3-1,ObjectSerializer writer = getObjectWriter(clazz);
这个方法是根据入参类型获得序列化器,整体和反序列化器的生成一致,很多代码结构都是一样的。
3-2,writer.write(this, object, null, null, 0);
每个类型的序列化器都会实现com.alibaba.fastjson.serializer.ObjectSerializer#write。各自实现自己的拼装json字符串逻辑
4,out.toString();
返回json字符串。
参照:http://www.csdn.net/article/2014-09-25/2821866