前言
学习!
环境搭建
依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
漏洞分析
autotype绕过漏洞,可以在不开启的情况下反序列化对象。
整个漏洞利用分为两个部分,参照1.2.68漏洞的想法反序列化内置类,据说是通过内置类setter、构造函数、field跟攻击用类的关系将攻击用类加入缓存,最后使用攻击用类完成攻击。
autotype绕过
内置类表
关注checkAutoType函数,黑白名单之后会从内置类表、缓存类表等地方先找类,内置类表使用TypeUtils.getClassFromMapping中查找:
clazz = TypeUtils.getClassFromMapping(typeName);
即:
public static Class<?> getClassFromMapping(String className) {
return mappings.get(className);
}
Exception就在这个内置类表中,所以并直接返回继续反序列化:
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;
}
缓存
写两个对象:
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) {
}
}
然后是测试用代码:
String json1 =
" {\"a\":{" +
" \"@type\":\"java.lang.Exception\"," +
" \"@type\":\"org.example.MyException\"," +
" \"clazz\":{}" +
" }}";
try {
JSON.parse(json1);
}catch (Exception e) {
// pass
}
Fastjson根据Exception类找到对应的反序列化器ThrowableDeserializer:
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所以类加载正常进行:
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
加载类后反序列化,首先是遍历构造函数实例化对象:
ex = createException(message, cause, exClass);
但是这里对构造函数的参数有要求:
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进去,首先找到对应的反序列化器:
ObjectDeserializer exDeser = parser.getConfig().getDeserializer(exClass);
找到ThrowableDeserializer:
else if (Throwable.class.isAssignableFrom(clazz)) {
deserializer = new ThrowableDeserializer(this, clazz);
}
构造函数会调用JavaBeanInfo.build构建成员信息,然后反序列化field:
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函数:
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);
}
关键代码:
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函数调用,不同的是由于找不到对应的反序列化器,所以会创建:
deserializer = createJavaBeanDeserializer(clazz, type);
...
putDeserializer(type, deserializer);
这就导致了org.example.MyClass在deserializers中有了自己对应的反序列化缓存,下次反序列化时流程就跟Exception类似:
// Exception走这里
clazz = TypeUtils.getClassFromMapping(typeName);
// org.example.MyClass走这里
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
也就绕过了autotype。
把field、setter和构造函数分开一个个测试,会发现以下两种情况可以成功利用:
构造函数
setter
只有field的情况下,调试一下因为它获取field的函数用的是getFields,所以要改成public形式的:
class MyException extends Throwable {
public MyClass clazz;
public MyException(MyClass clazz) {
}
public void setClazz(MyClass clazz) {
}
}
也就是说field、setter和构造函数都可以绕过,最后调试一下构造函数的绕过点,给putDeserializer函数下断点,找到JavaBeanInfo.build函数:
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的利用链。
参考
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!