关于反序列化的思考
title: 关于反序列化的思考 description: 反序列化学习笔记 date: 2026-04-26 tag:
- Java
- Security author: me7eorite readingTime: true
问题
刚开始学 Java 反序列化时,很容易被各种链劝退。比如 CC1-CC8、CB、ROME、Hessian、Shiro、Fastjson,每一条链看起来都有很长的调用路径。如果只是按编号去背:
CC1 是怎么调用?有什么限制?
CC2 是怎么调用?有什么限制?
CC6 是怎么调用?有什么限制?短期看起来好像学了很多,笔记的文件也在增加,收集了很多调用栈,但过一段时间很容易忘。尤其在 AI 时代,直接问一句“帮我总结一下 CC 链”,AI 可以生成一篇很完整的文章,但是问题在于:
- 我真的理解了吗?
- 我能够举一反三吗?
- 我能够检查AI出错的时候吗?
观点
学习反序列化链,不应该只盯着某一条链的完整调用过程,应该思考学习的目的,为的是建立反序列化链的模型。 一些文章常常会读到:Source -> Sink 的写法,但是这个路径太短,不适合学习的时候用,可以参考:
Source -> Trigger -> Bridge -> Propagator -> Sink
Source -> Trigger -> (Bridge Gadget -> Propagator Gadget -> Sink Gadget -> Sink)这五层分别对应:
- Source:反序列化时自动执行逻辑的入口。
- Trigger:Source 自动调用的方法,例如
hashCode、compare、toString、setValue。 - Bridge:将 Trigger 调用转接到 gadget 链的关键对象。
- Propagator:继续传播调用的对象。
- Sink:最终产生危险效果的方法。
这样看一条链时,就不是记一长串方法名,而是判断每一层分别由谁承担,后续了解到 CodeQL 也可以继续利用这个思路。
方法
CommonsCollections 系列最容易让人误解的地方是:看起来 CC1、CC2、CC3、CC4、CC5、CC6、CC7 都是不同的链,但从分层模型看,它们更多是在不同层里换组件,以下以 CC 链中出现的为例:
Source:
AnnotationInvocationHandler:CC1 / CC3HashMap/HashSet:CC6 / CC3PriorityQueue:CC2 / CC4TreeBag / TreeMap:CC4 变体BadAttributeValueExpException:CC5Hashtable:CC7
因为高版本 JDK 改了 AnnotationInvocationHandler 的实现,CC1 原来的触发方式不再稳定。所以更换 Source,本质上是在找新的自动调用点,减少对特定 JDK 版本的依赖,不同 Source 会带来不同 Trigger:
HashMap会触发 key 的hashCodePriorityQueue会触发 comparator 的compareBadAttributeValueExpException会触发toStringAnnotationInvocationHandler会触发 Map 相关操作
Bridge 和 Propagator(其实还可以细分):
TransformedMap:把setValue/ 写入 value 转成Transformer#transformLazyMap:把get转成Transformer#transformTiedMapEntry:把hashCode/toString转成getValue -> map.getTransformingComparator:把compare转成Transformer#transform
这里要区分一个点: TiedMapEntry 和 LazyMap 经常一起出现,但它们角色不同。
- CC6 :
HashMap#hashcode -> TiedMapEntry#hashcode -> LazyMap#get - CC5 :
BadAttributeValueExpException#toString -> TiedMapEntry#toString -> LazyMap#get简而言之,CC5 和 CC6 的中后段其实一样,只是 Trigger 不一样。
Sink 也可以进行区分,有些类本身不是真正的危险点,而是 "Sink的调用器",可以称为 Sink Gadget。 例如:
InvokerTransformer:实现反射调用InstantiateTransformer + TrAXFilter:实现实例化类
真正的危险点是 Real Sink:
Runtime.execTemplatesImpl#newTransformerTemplatesImpl#defineTransletClasses / defineClass
举个例子: InvokerTransformer 不是最终 Sink,它只是反射调用工具。真正的 Sink 要看它最后调用的是 Runtime.exec,还是 TemplatesImpl#newTransformer。
拓展到 CB 和 ROME
这个模型不只适用于 CC,CB 链也可以分析:
Source:PriorityQueue
Trigger:comparator.compare
Bridge:BeanComparator
Propagator:PropertyUtils.getProperty -> getter
Sink:TemplatesImpl#newTransformerCB 的重点不是 Transformer,而是 JavaBean 属性访问:
compare -> getProperty -> getter -> sinkROME 链也类似:
- Source:HashMa
- Trigger:hashCode
- Bridge:EqualsBean / ObjectBean
- Propagator:ToStringBean -> JavaBean getter
- Sink:TemplatesImpl#newTransformer
ROME 的关键点是:
hashCode / equals / toString -> JavaBean getter -> sink所以从 CC 到 CB,再到 ROME,可以看到一个共同思路:
- 反序列化入口自动调用某个普通方法
- 普通方法被 Bridge 实现
- 调用进行传播
- 最后触发危险方法
区别在于中间的传播方式不同:
- CC 主要围绕
Transformer(如何调用到transform方法) - CB 主要围绕
PropertyUtils和 getter - ROME 主要围绕
EqualsBean、ToStringBean、ObjectBean和 getter
结论
AI 可以快速生成大量链路总结,最快速地检索到大量资料,建立一个框架并以最大能力去往框架里补充和完善内容,我认为是提高学习效率的一个方式。 以反序列化链为例,通过对结构进行拆分:
Source -> Trigger -> Bridge -> Propagator -> Sink后续学新链的时候可以补充这个结构,在遇到某个特殊的 Source/Trigger/Sink 或许可以联想到这个在 XX 是否可用,而不是简单的背诵和调试堆栈。
