前言

学习!


环境搭建

依赖:

<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的利用链。


参考

某json 1.2.80 漏洞分析

Fastjson1.2.80漏洞复现