前言
学习!
环境搭建
依赖:
1 2 3 4 5
| <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.80</version> </dependency>
|
漏洞分析
autotype绕过漏洞,可以在不开启的情况下反序列化对象。
整个漏洞利用分为两个部分,参照1.2.68漏洞的想法反序列化内置类,据说是通过内置类setter、构造函数、field跟攻击用类的关系将攻击用类加入缓存,最后使用攻击用类完成攻击。
autotype绕过
内置类表
关注checkAutoType函数,黑白名单之后会从内置类表、缓存类表等地方先找类,内置类表使用TypeUtils.getClassFromMapping中查找:
1
| clazz = TypeUtils.getClassFromMapping(typeName);
|
即:
1 2 3
| public static Class<?> getClassFromMapping(String className) { return mappings.get(className); }
|
Exception就在这个内置类表中,所以并直接返回继续反序列化:
1 2 3 4 5 6 7 8 9 10
| if (clazz != null) { if (expectClass != null && clazz != java.util.HashMap.class && clazz != java.util.LinkedHashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); }
return clazz; }
|
缓存
写两个对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class MyClass { public void setName(String name) { System.out.println("Set Name"); } }
class MyException extends Throwable { private MyClass clazz;
public MyException(MyClass clazz) {
}
public void setClazz(MyClass clazz) {
} }
|
然后是测试用代码:
1 2 3 4 5 6 7 8 9 10 11
| String json1 = " {\"a\":{" + " \"@type\":\"java.lang.Exception\"," + " \"@type\":\"org.example.MyException\"," + " \"clazz\":{}" + " }}"; try { JSON.parse(json1); }catch (Exception e) { }
|
Fastjson根据Exception类找到对应的反序列化器ThrowableDeserializer:
1 2 3 4 5 6 7 8 9
| if (JSON.DEFAULT_TYPE_KEY.equals(key)) { if (lexer.token() == JSONToken.LITERAL_STRING) { String exClassName = lexer.stringVal(); exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures()); } else { throw new JSONException("syntax error"); } lexer.nextToken(JSONToken.COMMA); }
|
调用checkAutoType函数加载org.example.MyException类,跟1.2.68版本的绕过方式相同,由于存在expectClass所以类加载正常进行:
1 2 3 4
| if (autoTypeSupport || jsonType || expectClassFlag) { boolean cacheClass = autoTypeSupport || jsonType; clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass); }
|
加载类后反序列化,首先是遍历构造函数实例化对象:
1
| ex = createException(message, cause, exClass);
|
但是这里对构造函数的参数有要求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Class<?>[] types = constructor.getParameterTypes(); if (types.length == 0) { defaultConstructor = constructor; continue; }
if (types.length == 1 && types[0] == String.class) { messageConstructor = constructor; continue; }
if (types.length == 2 && types[0] == String.class && types[1] == Throwable.class) { causeConstructor = constructor; continue; }
|
不然只会实例化一个Exception对象而不是输入的org.example.MyException,到这里为止通过构造函数参数类型似乎无法使用。
此时由于输入了clazz这个field,所以会开始准备set进去,首先找到对应的反序列化器:
1
| ObjectDeserializer exDeser = parser.getConfig().getDeserializer(exClass);
|
找到ThrowableDeserializer:
1 2 3
| else if (Throwable.class.isAssignableFrom(clazz)) { deserializer = new ThrowableDeserializer(this, clazz); }
|
构造函数会调用JavaBeanInfo.build构建成员信息,然后反序列化field:
1 2 3 4 5 6 7 8 9 10 11
| String key = entry.getKey(); Object value = entry.getValue();
FieldDeserializer fieldDeserializer = exBeanDeser.getFieldDeserializer(key); if (fieldDeserializer != null) { FieldInfo fieldInfo = fieldDeserializer.fieldInfo; if (!fieldInfo.fieldClass.isInstance(value)) { value = TypeUtils.cast(value, fieldInfo.fieldType, parser.getConfig()); } fieldDeserializer.setValue(ex, value); }
|
由于输入的clazz值为{},是一个JSONObject,跟成员类型不合,所以会调用TypeUtils.cast函数进行类型转换。
由于JSONObject继承自Map,所以走到castToJavaBean函数:
1 2 3 4 5 6 7 8 9 10 11
| if (obj instanceof Map) { if (clazz == Map.class) { return (T) obj; }
Map map = (Map) obj; if (clazz == Object.class && !map.containsKey(JSON.DEFAULT_TYPE_KEY)) { return (T) obj; } return castToJavaBean((Map<String, Object>) obj, clazz, config); }
|
关键代码:
1 2 3 4 5 6 7 8 9 10
| JavaBeanDeserializer javaBeanDeser = null; ObjectDeserializer deserializer = config.getDeserializer(clazz); if (deserializer instanceof JavaBeanDeserializer) { javaBeanDeser = (JavaBeanDeserializer) deserializer; }
if (javaBeanDeser == null) { throw new JSONException("can not get javaBeanDeserializer. " + clazz.getName()); } return (T) javaBeanDeser.createInstance(map, config);
|
找反序列化器,又是一串getDeserializer函数调用,不同的是由于找不到对应的反序列化器,所以会创建:
1 2 3
| deserializer = createJavaBeanDeserializer(clazz, type); ... putDeserializer(type, deserializer);
|
这就导致了org.example.MyClass在deserializers中有了自己对应的反序列化缓存,下次反序列化时流程就跟Exception类似:
1 2 3 4 5 6 7
| clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) { clazz = deserializers.findClass(typeName); }
|
也就绕过了autotype。
把field、setter和构造函数分开一个个测试,会发现以下两种情况可以成功利用:
只有field的情况下,调试一下因为它获取field的函数用的是getFields,所以要改成public形式的:
1 2 3 4 5 6 7 8 9 10 11
| class MyException extends Throwable { public MyClass clazz;
public MyException(MyClass clazz) {
}
public void setClazz(MyClass clazz) {
} }
|
也就是说field、setter和构造函数都可以绕过,最后调试一下构造函数的绕过点,给putDeserializer函数下断点,找到JavaBeanInfo.build函数:
1 2 3 4 5 6
| Class<?> fieldClass = types[i]; Type fieldType = creatorConstructor.getGenericParameterTypes()[i]; ... FieldInfo fieldInfo = new FieldInfo(paramName, clazz, fieldClass, fieldType, field, ordinal, serialzeFeatures, parserFeatures); add(fieldList, fieldInfo);
|
根据构造函数参数类型和参数名,直接新建了一个FieldInfo出来放到fieldList。
反序列化攻击
在能加载到的类里找到能利用的就行,参考文章有groovy、jdbc和aspectj的利用链。
参考
某json 1.2.80 漏洞分析
Fastjson1.2.80漏洞复现