fastjson 1.2.83 · 源码级

反序列化总调用栈

从一段 JSON 字符串,到对象实例化、setter / getter 触发,再到 RCE 的完整内部调用链。行号均对应真实源码。

setter 链 getter 链 安检墙 checkAutoType gadget 触发
Entry · 入口层

三个常用入口 API

殊途同归到 parse 核心,差别只在最底部的“收尾回转”

JSON.java:251JSON.parseObject(text)← 单参,末尾会 toJSON 回转 ★
JSON.java:274JSON.parseObject(text, Clazz)
JSON.java:238JSON.parse(text)
Parse · 解析驱动

JSON.parse(text, features)

JSON.java:228

new DefaultJSONParser(...)parser.parse(),构建词法器并进入解析。

State Machine · 状态机

DefaultJSONParser.parseObject()

逐 key 扫描

大状态机循环:读一个 key → 判断是否特殊键(@type / $ref)→ 分发。

?

key == "@type" ?

:315

是 → 取出类名 lexer.scanSymbol() :317 送安检;否 → 普通字段,直接汇入下方【赋值】。

typeName = "…JdbcRowSetImpl"
🛡️
ParserConfig.checkAutoType()
:1311
1.2.83 的唯一防线,也是全部战场 · DefaultJSONParser:343 处调用
safeMode?:1329
长度 <3 或 ≥192:1338
'[' 数组开头:1368
首+尾字符 hash:1372
internalDeny 黑名单:1386
accept 白名单 ✓放行:1402
deny hash 黑名单:1408
读 .class @JSONType:1489
loadClass 真正加载:1502
ClassLoader/DataSource/RowSet:1513
默认 autoTypeSupport = false:普通类走到 :1537autoType is not support —— 你的 Test 例子就死在这里。放行需开启 autoType 或命中白名单/绕过
返回 Class,放行
Deserializer · 取反序列化器

config.getDeserializer(clazz)

:396

ParserConfig.createJavaBeanDeserializer() :935 决定用哪种反序列化器。

?

asmEnable ?

:936
TRUE · 默认

ASM 动态生成

:1073asmFactory.create…

运行时拼字节码生成专用类,临时类加载器加载 —— IDEA 断点断不到

FALSE · 可调试

new JavaBeanDeserializer

:1068

纯反射版。有 getOnly 字段(如 getMap())命中 :1011 强制退化到这里 —— 你的调试技巧。

Build · 构建元信息

JavaBeanInfo.build()

反射扫描目标类,登记 setXxx / getXxxFieldInfo[],供下一步逐字段匹配。

Deserialize · 反序列化主体

JavaBeanDeserializer.deserialze()

:392

调用点 deserializer.deserialze(...) DefaultJSONParser:405

Instantiate · 实例化

createInstance()

:190

constructor.newInstance() :214 —— 无参构造被调用(gadget 类需可访问无参构造)。

逐字段赋值 · setValue
Assign · 字段赋值(触发点)

FieldDeserializer.setValue()

:59

入口 DefaultFieldDeserializer.setValue() :126 —— 这里分出两条触发路径

SETTER 链

字段有 setter

method.invoke(obj, value):167

setXxx(value) 被反射调用。

💥
JdbcRowSetImpl · setDataSourceName + setAutoCommit → JNDI lookup
GETTER 链-A · 次

getOnly + 返回 Map/Collection

method.invoke(obj):74-132

为把元素塞进集合,先调 getter 拿容器再 putAll。

💥
TemplatesImpl · getOutputProperties() → 加载字节码
deserialze 返回 一个具体类实例(注意:不是 JSONObject)
仅 parseObject(text) 单参
?

obj instanceof JSONObject ?

:253

@type 造出了具体 bean,类型对不上 → 走 分支,强制转回 JSONObject。

Rebound · 收尾回转(getter 链-B · 主)

JSON.toJSON(obj)

:258 → :1127

getFieldValuesMap() :1221 遍历每一个 getterFieldSerializer.getPropertyValueFieldInfo.getmethod.invoke(obj)

💥
TemplatesImpl · 这条才是 parseObject(text) 下 getter 型 gadget 的主触发路径(你文档 §5/§6)
A

必须记住的三个分叉

墙 · ASM · getter 两处
🛡️

安检墙 checkAutoType

唯一防线。默认 autoTypeSupport=false,普通类在 :1537 被拒。所有 RCE 本质都是“如何让墙放行一个能打的类”。

⚙️

ASM 分叉 :936

ASM 动态类(默认,断点断不到)vs 反射版(可调试)。加一个 getOnly 字段命中 :1011 强制走反射版 —— 只是调试技巧,不改漏洞逻辑。

🔁

getter 的两个位置

A·填充字段时 FieldDeserializer:74-132;B·收尾回转 JSON:258→1221B 是主路径,你 §5/§6 正是这条。

B

两条经典 gadget 走图里哪条路

SETTER 链

JdbcRowSetImpl

JNDI 注入 · 入口 API 任意
  • @type 放行 checkAutoType
  • 实例化 newInstance :214
  • setValue invoke(obj,value) :167
  • setDataSourceName / setAutoCommit
  • JNDI lookup → RMI/LDAP 💥
额外前提:目标能出网打 RMI/LDAP。
GETTER 链-B

TemplatesImpl

加载恶意字节码 · 入口用 parseObject(text)
  • @type 放行(+设私有字段)
  • 实例化 newInstance :214
  • 返回具体 bean,非 JSONObject
  • 收尾 toJSON :258 → getFieldValuesMap :1221
  • getOutputProperties → newTransformer 💥
额外前提:Feature.SupportNonPublicField 给私有字段赋值。
C

一句话流水线

便于口头复述
parseObject parse parseObject(状态机) @type checkAutoType(墙) getDeserializer(asm/反射) deserialze newInstance setValue(调 setter) 【单参回转】toJSON → getFieldValuesMap(调 getter) 返回 JSONObject