Jackson对不安全反序列化的处理

2,050 阅读7分钟

背景

在查看公司SpringBoot项目代码的时候,发现RedisTemplate的序列化配置有两种:

使用GenericJackson2JsonRedisSerializer和使用Jackson2JsonRedisSerializer,于是就简单看了下使用区别,大致是GenericJackson2JsonRedisSerializer序列化后会带有@class属性,该属性的值是ClassName,反序列化可直接强转为对应的对象,而Jackson2JsonRedisSerializer序列化后进行反序列化不可强转为对应的对象,因为反序列化后的类型为LinkedHashMap。

但是实际使用中,并没有感觉有什么区别,于是看到了类似于下面这个代码的配置,其作用就有序列化后会带有ClassName,反序列化可直接强转为对应的对象。

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

再查看这段代码的时候,对于这个LaissezFaireSubTypeValidator的作用不太清楚,于是有了下面的内容。

Jackson使用了两种方式防止不安全的序列化

黑名单

Jackson在SubTypeValidator类中明确禁止某些类反序列化,可自行查看

白名单

Jackson提供了PolymorphicTypeValidator类,可用以进行实现更具选择性的白名单方法,也就是只有列入白名单的类的对象才会被反序列化。

PolymorphicTypeValidator从名字来看是多态类型验证器,作用是解决反序列化漏洞——远程代码执行等漏洞,同时解决在多态的情况下(反序列化需要指定类型ID,用于区分创建子类对象实例),可能会出现伪造类型ID,创建恶意的对象实例,进行恶意操作。

Jackson为什么要发布2.10,以及在2.10中引入反序列化白名单

关于为什么多态性会出问题?

比如Object类型、Serializable、Comparable类型的属性。

如果在接受外界输入内容的类中有Object类型的属性,那么攻击者可以从公布的反序列化漏洞直接选择并使用,构造指定类型ID(ClassName)的Json串进行反序列化,伪造对象实例,进行恶意操作。

基于Jackson漏洞攻击的条件有哪些?

  • 接受由不受信任的客户端发送的Json

  • 在程序已有的Java类中有对应的漏洞类可用

  • 程序中有类型为java.lang.Object的属性或其他标记接口的使用多态类型处理

  • 程序使用Jackson版本还没有将一些漏洞类等加入黑名单

Jackson做了哪些防止反序列化漏洞的措施?

简而言之就是黑名单机制和白名单机制

开发人员应该怎样主动去保护程序?

  • 及时更新Jackson的版本

  • 避免启用默认类型,也就是基于类型的全局默认类型模式,明确指定哪里需要多态性。

  • 避免使用Object和Serializable作为多态值的类型

  • 尽可能的使用@JsonTypeInfo(use = Id.NAME)而不是使用类路径名作为反序列化的类型ID。

Jackson的反序列化漏洞案例

Jackson dbcp gadget以及CVE-2018-5968_隐形人真忙的博客-CSDN博客

Jackson JDOM XSLTransformer Gadget浅析 - 腾讯云开发者社区-腾讯云

blog.doyensec.com

从源码分析Jackson白名单机制

PolymorphicTypeValidator的验证方法在何时被调用?

public abstract class PolymorphicTypeValidator
    implements java.io.Serializable
{
    private static final long serialVersionUID = 1L;

    public abstract Validity validateBaseType(MapperConfig<?> config, JavaType baseType);

    public abstract Validity validateSubClassName(MapperConfig<?> config, JavaType baseType,
            String subClassName) throws JsonMappingException;

    public abstract Validity validateSubType(MapperConfig<?> config, JavaType baseType,
            JavaType subType) throws JsonMappingException;
}

validateBaseType

validateBaseType方法被buildTypeDeserializer调用,用于验证基类型(反序列化子类则是父类型,无父类则是本身类型),目的是为了尽早确认子类型是否被拒绝反序列化或者说尽早确认子类型是否允许反序列化(简而言之就是如果父类型被拒绝序列化,那么子类型也一定不允许序列化,父类型允许序列化,那么子类型也一定允许序列化,这点需要注意)

validateSubClassName

validateSubClassName方法被resolveAndValidateSubType方法调用(也就是当前对象Json串中的TypeId——ClassName被读取出来验证),用于验证子类型,目的是在解析出子类型之前尽可能的通过读取ClassName进行确认是否允许序列化。

validateSubType

validateSubType方法被resolveAndValidateSubType方法调用,用于验证子类型或者说当前类型,通过ClassName获取Class对象,在获取其验证后的子类型,用于确认是否允许序列化。

验证方法的顺序和返回值

上述三个方法的返回值有以下三种:

    public enum Validity {
        ALLOWED,
        DENIED,
        INDETERMINATE;
    }

ALLOWED

表示允许此类进行反序列化,不需要进行下一步检查

DENIED

表示不允许此类进行反序列化,不需要进行下一步检查

INDETERMINATE

表示此类无法由当前验证方法进行确认是否允许序列化,需要进一步的验证

以上三个方法的的执行顺序为validateBaseType,当此方法返回INDETERMINATE,则继续向下调用validateSubClassName,当此方法返回INDETERMINATE,则继续调用validateSubType。

PolymorphicTypeValidator的子类

LaissezFaireSubTypeValidator

通常只在对于输入的内容完全受信任时使用,因为其不执行任何验证,比如从Redis反序列化。

public final class LaissezFaireSubTypeValidator
    extends PolymorphicTypeValidator.Base
{
    public final static LaissezFaireSubTypeValidator instance = new LaissezFaireSubTypeValidator(); 

    @Override
    public Validity validateBaseType(MapperConfig<?> ctxt, JavaType baseType) {
        return Validity.INDETERMINATE;
    }

    @Override
    public Validity validateSubClassName(MapperConfig<?> ctxt,
            JavaType baseType, String subClassName) {
        return Validity.ALLOWED;
    }

    @Override
    public Validity validateSubType(MapperConfig<?> ctxt, JavaType baseType,
            JavaType subType) {
        return Validity.ALLOWED;
    }
}

BasicPolymorphicTypeValidator

最常用类型验证器,常用于构建白名单。

public class BasicPolymorphicTypeValidator extends PolymorphicTypeValidator.Base
        implements java.io.Serializable
{
    /**
     * 判断基类型是否在无效的基类型列表中,在则表示不允许进行反序列化,
     * 不在则判断是否在允许的基类型列表中,在则表示允许进行反序列化,
     * 以上条件都不满足,则本方法无法验证,须经下一步验证。
     */
    @Override
    public Validity validateBaseType(MapperConfig<?> ctxt, JavaType baseType) {
        final Class<?> rawBase = baseType.getRawClass();
        if (_invalidBaseTypes != null) {
            if (_invalidBaseTypes.contains(rawBase)) {
                return Validity.DENIED;
            }
        }
        if (_baseTypeMatchers != null) {
            for (TypeMatcher m : _baseTypeMatchers) {
                if (m.match(ctxt, rawBase)) {
                    return Validity.ALLOWED;
                }
            }
        }
        return Validity.INDETERMINATE;
    }
    /**
     * 判断从Json串中的读取TypeId——ClassName,是否在允许的子类型列表中,在则表示允许进行反序列化,
     * 不满足以上条件,则进行下一步验证
     */
    @Override
    public Validity validateSubClassName(MapperConfig<?> ctxt, JavaType baseType,
                                         String subClassName)
            throws JsonMappingException
    {
        if (_subTypeNameMatchers != null)  {
            for (NameMatcher m : _subTypeNameMatchers) {
                if (m.match(ctxt, subClassName)) {
                    return Validity.ALLOWED;
                }
            }
        }
        return Validity.INDETERMINATE;
    }

    /**
     * 判断通过ClassName获取Class对象,在获取其验证后的子类型是否在允许的子类型列表中,在则表示允许进行反序列化,
     * 不满足以上条件,则进行下一步验证,通常的实现是拒绝反序列化。
     */
    @Override
    public Validity validateSubType(MapperConfig<?> ctxt, JavaType baseType, JavaType subType)
            throws JsonMappingException
    {
        if (_subClassMatchers != null)  {
            final Class<?> subClass = subType.getRawClass();
            for (TypeMatcher m : _subClassMatchers) {
                if (m.match(ctxt, subClass)) {
                    return Validity.ALLOWED;
                }
            }
        }
        return Validity.INDETERMINATE;
    }
}

DefaultBaseTypeLimitingValidator

通过自有的黑名单列表,排除一些不安全的类进行反序列化。

public class DefaultBaseTypeLimitingValidator extends PolymorphicTypeValidator
    implements java.io.Serializable
{
    @Override
    public Validity validateBaseType(MapperConfig<?> config, JavaType baseType)
    {
        if (isUnsafeBaseType(config, baseType)) {
            return Validity.DENIED;
        }
        return Validity.INDETERMINATE;
    }

    @Override
    public Validity validateSubClassName(MapperConfig<?> config,
            JavaType baseType, String subClassName) {
        return Validity.INDETERMINATE;
    }

    @Override
    public Validity validateSubType(MapperConfig<?> config, JavaType baseType,
            JavaType subType)
    {
        return isSafeSubType(config, baseType, subType)
                ? Validity.ALLOWED
                : Validity.DENIED;
    }

    protected boolean isUnsafeBaseType(MapperConfig<?> config, JavaType baseType)
    {
        return UnsafeBaseTypes.instance.isUnsafeBaseType(baseType.getRawClass());
    }

    protected boolean isSafeSubType(MapperConfig<?> config,
            JavaType baseType, JavaType subType)
    {
        return true;
    }

    private final static class UnsafeBaseTypes {
        public final static UnsafeBaseTypes instance = new UnsafeBaseTypes();

        private final Set<String> UNSAFE = new HashSet<>();
        {
            // first add names of types in `java.base`
            UNSAFE.add(Object.class.getName());
            UNSAFE.add(java.io.Closeable.class.getName());
            UNSAFE.add(java.io.Serializable.class.getName());
            UNSAFE.add(AutoCloseable.class.getName());
            UNSAFE.add(Cloneable.class.getName());

            // and then couple others typically included in JDK, but that we
            // prefer not adding direct reference to
            UNSAFE.add("java.util.logging.Handler");
            UNSAFE.add("javax.naming.Referenceable");
            UNSAFE.add("javax.sql.DataSource");
        }
        
        public boolean isUnsafeBaseType(Class<?> rawBaseType)
        {
            return UNSAFE.contains(rawBaseType.getName());
        }
    }
}

构建一个基本的白名单

	ObjectMapper om = new ObjectMapper();
        BasicPolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
                // 信任 com.xxx.等包下的类和某个类
                .allowIfBaseType("com.xxx.")
                .allowIfBaseType(yyy.class)
                // 信任 Collection、Map 等基础数据结构
                .allowIfSubType(Collection.class)
                .allowIfSubType(Number.class)
                .allowIfSubType(Map.class)
                .allowIfSubType(Temporal.class)
                .allowIfSubTypeIsArray()
                .build();
        om.activateDefaultTyping(validator,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

ObjectMapper.DefaultTyping枚举类

还枚举类是用以配置的哪些类型的属性受到影响,也就是会在Json中增添类型名

public enum DefaultTyping {
    //只影响Object类型的属性,也就是Object类型的属性序列化后会增添其实际类型名
    JAVA_LANG_OBJECT,
    //影响Object、Abstract、Interface类型的属性,序列化后会增添其实际类型名
    OBJECT_AND_NON_CONCRETE,
    //包含上述的类型的属性,还影响上述全部类型的数组类型,也即是支持了数组类型
    NON_CONCRETE_AND_ARRAYS,
    //影响所有未声明为final的类型,以及非final元素类型的数组类型
    NON_FINAL,
    /**
     * 影响所有类型,可以为任何类型添加TypeId,会导致在不需要添加类型信息的时候添加类型从而导致出错,
     * 比如被final修饰的Long、Double等类型
     */
    EVERYTHING
}

相关资料

cowtowncoder.medium.com 关于2.10中引入的PolymorphicTypeValidator的使用说明

若是哪里有理解错误的或写错的地方,望各位读者评论或者私信指正,不胜感激。