拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

入口点与触发点:ysoserial四条CC链(CC2/CC4/CC5/CC7)构造逻辑拆解

入口点与触发点:ysoserial四条CC链(CC2/CC4/CC5/CC7)构造逻辑拆解 开头写什么呢我琢磨了很久。很多朋友拿到 ys 项目ysoserial 这类工具的第一反应就是背链子CC1 用什么入口、CC6 用什么入口、链子长什么样。但真正到了自己分析、甚至要改链的时候发现背下来的那套根本用不上。原因很简单链的本质是入口点和触发点的组合关系而不是一串固定的类名。你只要把这两个点之间的关系吃透CC2、CC4、CC5、CC7 这些看起来乱七八糟的链其实都是在同一个骨架上换零件而已。这篇文章我想从一个防御者和分析者的双重视角把 Ys 项目ysoserial里四条典型 CC 链——CC2、CC4、CC5、CC7——的构造逻辑完整拆一遍。重点不是让你记住每条链的类名序列而是搞清楚三件事入口点改的是什么、触发点改的是什么、为什么这么改。搞懂这三件事之后你不仅能看懂现成的链还能自己分析未知链。对做 Java 安全防御的朋友来说这也是理解反序列化检测规则底层逻辑最快的一条路。1. 为什么是 Ys 项目Gadget 链分析的参考系1.1 Ys 项目在链分析中的角色Ys 项目ysoserial在 Java 反序列化攻防研究里的地位基本等同于参考答案。它把大量公开或自己挖掘的利用链封装成统一的命令行工具每条链都有完整的构造逻辑和触发验证。平时做代码审计、做 WAF 规则验证、做 RASP 绕过测试拿它当基准样本非常方便。但问题也出在这太多人把它当黑盒用java -jar ysoserial.jar CommonsCollections2 cmd跑一下弹个计算器就当自己会了。等遇到高版本 JDK、遇到过滤了某个类的场景就完全不知道从哪下手。真正有价值的用法是把 Ys 项目里每条链的源代码打开一行一行看它怎么构造入口对象、怎么安排触发路径。你会发现它里面的很多写法其实不是最优解而是在当时过滤条件下最通用的解。这个为什么这么选的推理过程才是链分析能力的来源。1.2 读链的正确姿势从 readObject 到 transform一条 Gadget 链的标准读法我一般分四步找入口哪个类重写了readObject、readResolve或toString、hashCode、equals这类半入口方法。这是反序列化时 JVM 会主动调用的地方。追调用从入口方法往下追看它调用了哪些外部方法。重点找toString、get、compare、equals、invoke这类多态派发的关键字。找触发点Sink最终执行危险操作的节点。在 CC 链里一般表现为Transformer.transform()方法——它本质上是传一个对象进去返回一个对象出来而我们可以让它返回任意命令执行的结果。连接入口和触发点中间需要一个桥接类把入口方法里的一次普通调用路由到触发点的危险方法上。这套四步法听起来简单但实操中每一步都有陷阱。比如入口方法调用的可能是this.val.toString()而这个this.val是我们可控的TiedMapEntry对象也可能调用的是comparator.compare(a, b)而这个comparator是我们可控的TransformingComparator。这其实就是 CC5 和 CC2 最核心的差异点后面我会详细展开。2. 四条链的骨架对比CC2/CC4/CC5/CC7 其实只改了两环2.1 一张表看懂四条链先给一张总表把四条链的入口点 → 中间环节 → 触发点列出来。这张表是我自己分析时用的写出来供大家参考链名称入口点Entry中间桥接Bridge触发点Trigger/Sink依赖版本CC2PriorityQueue.readObjectTransformingComparator.compareInvokerTransformer.transformCommonsCollections 4.xCC4PriorityQueue.readObjectTransformingComparator.compare InstantiateTransformerTemplatesImpl.newTransformerCommonsCollections 4.xCC5BadAttributeValueExpException.readObjectTiedMapEntry.toString → LazyMap.getChainedTransformer.transformCommonsCollections 3.xCC7Hashtable.readObjectLazyMap.gethashCode 碰撞触发 equalsChainedTransformer.transformCommonsCollections 3.x看到没有CC2 和 CC4 的入口点完全一样都是PriorityQueue.readObject区别只在触发点——后者把InvokerTransformer换成了InstantiateTransformer配合TemplatesImpl。CC5 和 CC7 的触发点也几乎一样都是走LazyMap.get到ChainedTransformer.transform但入口和桥接完全不同。2.2 共性骨架Transformer 数组与 LazyMap四条链里有三条都用到了一个核心数据结构Transformer数组 ChainedTransformer。这个组合的作用是创建一个命令链把一个对象依次经过多个 Transformer 处理最后变成我们希望的结果。举个例子一个典型的 ChainedTransformer 链可以这样理解第一个 Transformer 取Runtime类ConstantTransformer第二个 Transformer 调用getRuntime方法InvokerTransformer第三个 Transformer 调用exec方法InvokerTransformer。这样整个链的语义就是执行命令。这个结构非常关键因为它是触发点的最终目的地——不管入口怎么改、桥接怎么换最后都要落到这串 Transformer 上。LazyMap就更巧妙了。它继承自AbstractMapDecorator当调用get(key)发现 key 不存在时会调用预先设定的Transformer.transform(key)把 key 转换成一个新值。这就把一次 Map 读取操作变成了一次任意 Transformer 执行。在 CC5 和 CC7 里LazyMap就是那个把普通方法调用路由到危险方法的关键桥接。2.3 差异点入口类与触发函数的选择四条链真正的差异其实就两个维度。第一个维度是入口类不同PriorityQueue重写了readObject反序列化后必然执行heapify排序BadAttributeValueExpException重写了readObject会把字段值toString出来Hashtable重写了readObject恢复时会重新计算每个 key 的 hash 并处理碰撞。第二个维度是触发函数不同InvokerTransformer.transform是直接反射调用任意方法InstantiateTransformer.transform是实例化任意类TemplatesImpl.newTransformer是加载字节码。你可以这么理解入口点决定了链从哪里被启动触发点决定了链最终以什么方式执行代码。而桥接层要解决的就是一个问题——入口方法里那一次普通调用怎么精准地踩中LazyMap.get或者TransformingComparator.compare这些能被利用的中间方法。理解了这三个角色任何一条链在你眼里都不再是死记硬背的类名堆砌。3. CC2 与 CC4PriorityQueue 入口下的两种变形3.1 CC2 的最小链PriorityQueue TransformingComparator InvokerTransformerCC2 是我个人觉得最适合入门的一条链因为它的调用路径最短、最直观。它的入口是PriorityQueue——Java 集合框架里的优先队列本身重写了readObject()反序列化恢复队列元素后会调用heapify()重建堆结构。堆排序需要比较元素大小而比较动作交给了comparator.compare()。这个comparator是 PriorityQueue 的一个字段完全可控。所以 CC2 的构造思路是把comparator设为一个TransformingComparator这个类的compare(a, b)方法会直接调用transformer.transform(a)。那我们只要在这个transformer上挂一串执行命令的InvokerTransformer链整个链路就通了。简化示意如下PriorityQueue.readObject() → heapify() → siftDownUsingComparator() → TransformingComparator.compare() → InvokerTransformer.transform() → Runtime.exec()这个链的精妙之处在于它完全绕过了 CC1 依赖的AnnotationInvocationHandler。很多人说 CC2 是为了对抗InvokerTransformer被过滤的场景——其实不对CC2 本身就还在用InvokerTransformer。它的真正价值是把入口从AnnotationInvocationHandlerCommonCollections 3.x 的专属类换成了 JDK 自带的PriorityQueue从而让链在 CommonsCollections 4.x 里也能成立。这是个很关键的认知校正。3.2 CC4 的关键改动为什么把 InvokerTransformer 换掉CC4 是从 CC2 演化来的对应的是InvokerTransformer类被加入黑名单之后的场景。为什么InvokerTransformer会被重点关注因为它的transform()方法直接支持通过反射调用任意类的任意方法这在利用链命名空间里几乎是万能钥匙。当过滤器开始逐个排查 Transformer 类时第一个被盯上的就是它。CC4 的思路是用两个类的组合来替代InvokerTransformer的功能InstantiateTransformerTrAXFilter。InstantiateTransformer.transform()可以实例化任意类且支持传入构造函数参数。如果我们让它实例化TrAXFilter那么这个类的构造函数内部会把传入的TemplatesImpl对象交给newTransformer()方法。而TemplatesImpl.newTransformer()会加载它内部存储的字节码——这段字节码我们可以完全控制。所以触发点就从反射调用任意方法变成了加载自定义字节码语义等价但用的类完全不同。PriorityQueue.readObject() → TransformingComparator.compare() → InstantiateTransformer.transform() → TrAXFilter 构造函数 → TemplatesImpl.newTransformer() → 字节码加载执行这一步改动在攻防两端都有意义。攻击端获得了绕过InvokerTransformer黑名单的能力防御端则获得了一个新的检测特征——InstantiateTransformerTrAXFilter的组合调用。后面很多检测规则里出现TemplatesImpl相关关键字根源就在这条链上。3.3 构造时的顺序坑queue 元素必须在 compare 之前写好CC2 和 CC4 实操中最大的坑不在链结构本身而在PriorityQueue的序列化状态。PriorityQueue的readObject()会调heapify()而heapify()会调siftDown()过程中就会触发comparator.compare()。问题在于反序列化时比较器会对队列里的元素做真实比较如果你的元素和比较器不匹配会在利用链触发之前就抛出ClassCastException。所以构造时必须遵循一个先放元素、再设比较器的顺序。具体来说是先把队列add进两个占位对象比如Transformer数组的首元素和1此时队列正常排序然后把comparator换成TransformingComparator再通过反射把队列的elementData字段直接改成我们准备好的两个对象。这样序列化文件里记录的是正常队列 恶意比较器的状态反序列化触发时才会走到我们期望的路径上。这个细节在 Ys 项目源码里写得很清楚但很多人直接抄 payload 的时候根本不会注意到它的必要性。你自己写一遍构造代码就会明白每个字段的设置顺序都不是随意的而是对应着反序列化过程中某个具体的校验点。4. CC5 与 CC7不依赖 Transformer 版本的触发链4.1 CC5BadAttributeValueExpException 怎么把链叫醒如果说 CC2/CC4 走的是比较器触发路线那 CC5 走的就是toString 触发路线。入口类是BadAttributeValueExpException一个 JDK 自带的异常类重写了readObject()反序列化时会取字段val并调用它的toString()方法。注意这个val字段的类型是Object完全可控。所以构造思路就变成了让val指向一个TiedMapEntry对象。为什么要找TiedMapEntry因为这个类的toString()方法会调用getValue()而getValue()会调用map.get(key)。如果我们把map设置为一个LazyMap那map.get(key)就会触发LazyMap.get的默认行为——调用Transformer.transform(key)。到这里链就和前面的ChainedTransformer接上了。BadAttributeValueExpException.readObject() → val.toString() → TiedMapEntry.toString() → getValue() → map.get() → LazyMap.get() → ChainedTransformer.transform() → Runtime.exec()CC5 这条链有一个值得注意的版本差异在高版本 JDK 里BadAttributeValueExpException的readObject()逻辑做过修改不再是简单调val.toString()而是先判断val是否是String类型。这导致原版 CC5 在高版本 JDK 上直接失效。这也是分析链时一定要看的版本适配问题——链不是永远有效的JDK 一升级可能整条链就废了。4.2 CC7Hashtable 的 hashCode 碰撞利用CC7 是我个人认为思维跨度最大的一条链因为它用了一个非常冷门的入口机制Hashtable.readObject()在处理元素时会重新计算 hash如果发现两个 key 的 hash 相同就会调用equals()来确认。这里的关键是整个Hashtable对象在反序列化恢复时会对每个 key 执行reconstitutionPut而reconstitutionPut会调用key.hashCode()和equals()。构造思路是这样的准备两个LazyMap比如叫lazyMap1和lazyMap2它们的key是同一个字符串。在lazyMap1.get(key)被触发后LazyMap会把key对应的 value 设置为Transformer.transform(key)的处理结果——也就是我们命令链执行后的返回值。第一次调用后lazyMap1里会多出一个带恶意 value 的映射。而lazyMap2在equals()比较时调用get(key)此时 key 已存在返回的是恶意 value。虽然这中间的执行路径比较绕但核心目标只有一个让Hashtable在恢复数据的过程中对两个 key 执行一次看似正常的equals调用从而把控制流引导到LazyMap上。Hashtable.readObject() → reconstitutionPut() → key.hashCode() → 碰撞后调用 key.equals() → LazyMap.get() → ChainedTransformer.transform() → Runtime.exec()这里有一个经典的多线程坑CC7 利用的是两个LazyMap对象在hashCode过程中产生的动态变化。如果在构造时通过正常 API 向Hashtable里放元素LazyMap可能在被序列化之前就已经被污染——value 已经被transform处理过导致equals比较时不再走我们期望的路径。Ys 项目里 CC7 的构造方式其实用了一些反射手段绕开正常的 put 流程手动维护内部的table数组。这是为什么很多人照着网上帖子写 CC7 总是弹不出计算器的常见原因。4.3 为什么 CC7 要绕一圈 LazyMap你可能会问CC7 明明在Hashtable里绕了这么一大圈为什么不直接用更简单的入口答案是Hashtable是 JDK 自带的类而且readObject的实现比较傻——它老老实实地把恢复的元素重新put一遍过程中会调用hashCode、equals这些我们可控的方法。相比之下像HashMap的readObject也做类似的事情但它的处理路径在某些 JDK 版本下不可控比如HashMap的 hash 计算会先判断 key 是否为 null。所以 CC7 选Hashtable不是因为它最复杂而是因为它最稳定。另一方面绕一圈LazyMap还有一个实际收益LazyMap.get()在 key 不存在时会调用Transformer在 key 存在时直接返回已有 value。这个有则返回、无则转换的语义让攻击者可以在一次反序列化过程中控制转换发生的次数和转换发生的时机。这在构造需要多次触发或条件触发的链时非常有用。防御端理解这一点也很重要检测规则如果只盯着LazyMap.get这一个点很容易被其他入口类干扰但如果理解了Hashtable→hashCode→equals这条旁路就能把规则做得更精准。5. 入口点改动从 InvokerTransformer 到 TemplatesImpl 的演化逻辑5.1 入口点的本质谁能触发任意方法调用聊完四条链我们把视角拉高专门聊聊入口点改动这件事。我见过很多人把入口点理解成readObject 所在的类然后列表对比哪个类重写了 readObject。这个理解不够准确。真正值得关注的是这个入口点方法能不能在反序列化的天然流程中触发一次我们可控的、带多态派发的方法调用比如PriorityQueue.readObject触发的是comparator.compare()comparator是可控字段compare是接口方法可以被子类重写——这是入口点BadAttributeValueExpException.readObject触发的是val.toString()val是可控字段toString可以被任意类重写——这也是入口点。所以入口点改动的核心逻辑非常清晰找一个 JDK 自带的、实现了Serializable的类它的readObject会调用一个我们能控制实现的方法。方法本身不危险危险的是方法可以被重写这一点。这也是为什么AnnotationInvocationHandlerCC1 的入口在 CommonsCollections 4.x 里不可用的原因该类在 4.x 版本里被移到了org.apache.commons.collections4.functors内、且readObject的调用逻辑发生了变化——但根本原因其实是这个类的包名和可序列化状态在不同版本间不一致导致过滤器很容易识别。所以后来的链设计者开始大量使用 JDK 原生类作为入口因为这些类不受第三方库版本影响、也不容易被特征库命中。5.2 TemplatesImpl为什么它是攻防两端的共同焦点在所有触发点改动里绕不开的一个类就是TemplatesImpl。这个类本身不是 Transformer但它提供的newTransformer()方法可以加载内部字节码并执行而且是 JDK 自带的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl。这个类一出场触发点就从反射调用一个方法变成了加载一段字节码。前者的命令被限制在已有类的方法调用链路上后者可以直接执行任意构造的字节码灵活性完全不同。这个设计对后续几乎所有 Java 利用链都产生了影响——从 CC2 到 CC3、CC4再到后来的 CC6、CC7 变体大量链都把最终触发点改到了TemplatesImpl.newTransformer()上。所以你会发现很多反序列化防御规则的检测点不是 Transformer 了而是直接检测TemplatesImpl的_bytecodes字段、newTransformer方法调用链、以及TransletClassLoader的加载行为。对于分析者来说理解TemplatesImpl的触发逻辑才是理解为什么这么多链的最终形态都是字节码加载的关键。TemplatesImpl.newTransformer() → getTransletInstance() → 加载 _bytecodes 字段对应的字节码 → 实例化恶意类并执行构造逻辑这里要提醒一句TemplatesImpl利用条件比较苛刻其中一个前提是_bytecodes字段必须能被序列化写入。它本身继承了AbstractTranslet但很多版本的TemplatesImpl在反序列化时没有显式重写readObject这意味着_bytecodes会走默认序列化流程实际能被正常写入和读取。这也是它在利用链里如此稳定的原因之一。5.3 入口点改动的通用判断方法既然入口点改动如此重要那面对一个新的反序列化入口类怎么快速判断它能不能用我总结了一个四条件判断法分享给大家类必须实现Serializable且readObject或readResolve方法存在。这是基本门槛。readObject方法内部必须存在一个可控参数点。这个参数可能是字段值、类对象、集合元素但必须能被序列化数据控制。可控参数点必须流向一个多态调用。也就是通过接口或父类方法调用了某个可被重写的方法比如toString、hashCode、equals、compare、get、iterator。从多态调用到危险方法之间必须存在一条不经过安全检查的直达路径。如果中间有instanceof检查、类型判断、类加载限制就要考虑换到另一个入口或换一种桥接方式。这四个条件其实也是防御端检测规则的设计思路。你去看各类 RASP 的readObject调用链检测规则基本都是在追踪readObject → 可控参数 → 多态调用这条控制流路径。理解了入口点的本质很多检测规则的设计逻辑会变得非常清晰。6. 触发点改动改链不是拼接而是重新验证6.1 触发点判断的三个标准说完了入口点再来看触发点。很多人在改链时有个误区觉得触发点就是把InvokerTransformer换成InstantiateTransformer就完事了然后拼到链上直接跑。结果往往是各种奇奇怪怪的报错比如SerializationException、ClassCastException、IllegalArgumentException。判断一个触发点是否合适我一般用三个标准第一个标准是语义等价性。新的触发点是否实现了和旧触发点相同的代码执行语义InvokerTransformer能反射调用任意方法InstantiateTransformer能实例化任意类TemplatesImpl.newTransformer能加载字节码——它们的能力不同必须在新链里确认整个调用路径都能跑通。第二个标准是参数适配性。从入口到触发点的中间路径上每一步传递的参数类型是否匹配比如TransformingComparator.compare(a, b)会把a作为参数传给transform()那InstantiateTransformer.transform()接收的这个参数就必须能正确传入TrAXFilter构造函数。如果参数不匹配链会在运行期直接断裂。第三个标准是序列化兼容性。整个链上的所有对象在序列化和反序列化之后状态是否保持一致比如TransformComparator在 CommonsCollections 4.0 里需要实现Serializable如果版本不对序列化时会直接抛异常连传输阶段都过不了。6.2 替换触发点后的调试手段改完触发点之后第一个动作不会是打包丢进工具跑而是本地调试。我个人的习惯是写一个简单的 Java 独立类分三步验证第一步只构造触发点对象本身手动调用一次transform()方法确认触发点本身的逻辑是通的。这一步能快速排除触发点自身代码写错的问题。第二步构造完整链对象但不经过序列化直接在 JVM 内手动调用入口方法比如调用priorityQueue的heapify()确认链在内存态是通的。此时如果报错基本可以定位到桥接层。第三步把完整对象序列化到文件再反序列化回来触发。这一步验证的是序列化过程是否丢字段、反序列化是否有额外限制。很多链在内存态跑通但序列化再反序列化后就失效原因就是某些字段是transient或者readObject里有额外校验。调试的时候一定要加上-Dcom.sun.org.apache.xalan.internal.xsltc.trax.enableTemplatesImpltrue这类 JDK 参数吗不一定不同 JDK 版本的默认策略不同。但有一个参数是必加的-Dsun.misc.unsafe.enabletrue用不上真正需要关注的是序列化调试利器——在关键方法上打断点观察调用栈。这里我推荐直接看调用栈的深度确认是否从期望的入口方法一路走到了期望的触发点。6.3 一个典型的改动排查过程举个实际例子。我在分析一个针对高版本 JDK 的注入场景时原链用的入口是AnnotationInvocationHandler但目标环境对AnnotationInvocationHandler做了黑名单过滤。我的改法是把入口换成PriorityQueueTransformingComparator触发点保持InvokerTransformer不变。听起来很简单但实操中踩了两个坑。第一个坑是PriorityQueue的序列化导致Comparable类型不对。原链里队列元素是随意放的两个对象但PriorityQueue在heapify时会调用comparator.compare(a, b)此时如果两个元素类型不一致会抛ClassCastException。解决办法是把两个元素设为同一类型并且保证在compare中经过TransformingComparator处理后返回值是固定值。这个坑的本质是compare方法不是只调用一次在堆排序过程中会调用多次每次调用都可能对我们的对象做 transform 操作。第二个坑是InvokerTransformer在 CommonsCollections 4.x 里被移除了Serializable实现。对你没看错——CommonsCollections 4.0 开始把InvokerTransformer的序列化能力移除了后来又加回来如果用的版本不对整个链在序列化阶段就会直接失败。这个信息非常重要也是很多从 3.x 改 4.x 的人最容易忽略的版本陷阱。最终我选用了commons-collections 4.1版本才让链在序列化阶段通过了。这个排查过程想说明一个道理改链最核心的工作不是换类名而是重新验证整条链在目标环境下的序列化兼容性、参数类型和调用顺序。7. 防御视角链分析能力如何反哺检测与加固7.1 入口点特征重写 readObject 的类是重点聊了这么多攻击链分析最终要落到防御上。先说结论入口点分析对防御端的最大价值是为检测规则提供起点特征。反序列化检测本质上是在跟踪谁读入了恶意数据、谁被反序列化了。想知道入口点有哪些直接看 JDK 和第三方库里的Serializable类中哪些重写了readObject。我整理过一个优先级清单供防御端参考第一优先级JDK 内置且重写了readObject的集合类、异常类比如PriorityQueue、Hashtable、BadAttributeValueExpException、AnnotationInvocationHandler。这些类是绝大多数链的入口候选。第二优先级第三方库中的可序列化装饰器类比如LazyMap、TransformingComparator、TiedMapEntry。它们本身不是入口但承接了从入口到触发点的关键调用路径。第三优先级最终执行字节码加载或反射调用的类比如TemplatesImpl、InvokerTransformer、InstantiateTransformer。它们是真正的危险动作执行者。检测规则的设计思路就很清晰了不是单点检测readObject也不是单点检测TemplatesImpl而是把入口点、桥接点、触发点串成一条调用链特征。哪条链上出现了重写 readObject 的类 可控字段 多态调用 危险方法哪里就是可疑点。7.2 触发点特征危险方法调用链的检测思路触发点特征又是另一套逻辑。因为入口点可以被换掉但触发点的危险语义是相对稳定的——要么是反射调用任意方法要么是实例化外部类要么是加载自定义字节码。所以在检测层面识别触发点的价值比识别入口点更大。我推荐三个检测维度第一个维度是危险方法调用链长度。反序列化场景里从readObject到transform或newTransformer之间通常会有 3 到 6 层调用。如果一个对象的反序列化调用链特别干净——直接从 readObject 跳到了危险方法这往往是沙箱环境才会有的特征如果调用链在中间层出现了LazyMap.get或TransformingComparator.compare这类类名那基本可以判定是利用链。第二个维度是目标类组合模式。单独看PriorityQueue、TemplatesImpl、LazyMap都无害但它们的字段组合——比如PriorityQueue里装了TransformingComparatorLazyMap里装了ChainedTransformer——就是强烈的恶意信号。检测规则完全可以基于类组合特征建立指纹库。第三个维度是深拷贝前的对象状态。有些 RASP 的实现思路是在resolveClass阶段检查类黑名单在readObject入口进行调用链采样。如果能做到在反序列化完成前就拿到对象图结构就能在恶意代码执行之前拦截。这个思路对TemplatesImpl尤其有效——_bytecodes字段在对象图里是可见的一旦发现某个对象图里同时包含TemplatesImpl和Transformer类基本可以直接阻断。7.3 实践中建议的排查清单最后给一份我在实际做 Java 反序列化排查时会对照的清单算是一个经验总结排查项检查目标常见风险信号序列化入口类项目中所有重写readObject的类批量出现PriorityQueue、Hashtable等非业务类可控字段流向readObject中字段是否流向多态方法存在Object类型字段被直接toString、hashCode第三方库版本commons-collections、xalan 等版本版本过旧且包含可利用类危险类加载TemplatesImpl、InstantiateTransformer使用情况反序列化数据中出现_bytecodes字段过滤规则效果黑名单 vs 白名单黑名单只防了已知链白名单能阻止大部分未知链日志与调用链采样反序列化入口是否有完整调用栈记录缺少调用链上下文难以溯源这份清单不一定完备但每次排查基本都能覆盖 80% 的常见利用路径。最重要的是它不是一个静态清单——每次分析完新的利用链我都会回头更新它。因为利用链分析的本质是理解数据如何变成控制流而防御的本质是监控同样的路径。两边共用一套知识体系只是角度不同。代码写到这想起一开始那个问题——为什么要分析 Ys 项目里的链答案其实很简单不是为了背链而是为了掌握入口点改动和触发点改动的底层逻辑。当你理解了这两环任何一条链在你眼里都是入口点 × 桥接层 × 触发点三者的排列组合。以后再遇到陌生的链、需要改造的链、或者要防御的链你都能用同一套思维框架迅速拆解而不是依赖工具或现成 payload。这才是分析能力真正沉淀下来的地方。
返回列表