当前位置: 首页 > news >正文

CC6反序列化链

CC6:不受 JDK 版本限制的入口替换链

CC6 可以看成 CC1 的入口替换版。后半段的触发、串联和执行能力基本原样保留,变化点集中在反序列化入口:由谁在readObject()阶段把调用送进攻击者可控的Map

1. 适用条件

JDK版本:不受版本限制
组件版本:commons-collections 3.1 ~ 3.2.1

2. JDK 更新限制了 AnnotationInvocationHandler 的自动调用能力

CC1 链分为写时触发链和读时触发链,入口集中在同一个位置:sun.reflect.annotation.AnnotationInvocationHandler.readObject()(以下简称 AIH)。

两条链的区分在触发动作:写时触发链的入口动作是一次写入(entry.setValue(...)),读时触发链的入口动作是一次读取(LazyMap.get())。

AIH 的readObject()方法会遍历一张"注解成员名 -> 成员值"的表,并据此接入 CC1 的两条链:

  • 读时触发链:memberValues.entrySet()-> 触发 map 代理类的 AIHinvoke()->LazyMap.get()->ChainedTransformer.transform()
  • 写时触发链:成员值类型不匹配时调用entry.setValue(...)->TransformedMap.checkSetValue()->valueTransformer.transform()

这个方法在 JDK 8u71 被整体重写,两个版本对比如下。

JDK 7u21 中的实现:

s.defaultReadObject();...for(Map.Entry<String,Object>memberValue:memberValues.entrySet()){// ▶ 遍历攻击者可控的 memberValuesStringname=memberValue.getKey();Class<?>memberType=memberTypes.get(name);if(memberType!=null){Objectvalue=memberValue.getValue();if(!(memberType.isInstance(value)||valueinstanceofExceptionProxy)){memberValue.setValue(// ▶ 写时链触发点newAnnotationTypeMismatchExceptionProxy(value.getClass()+"["+value+"]").setMember(annotationType.members().get(name)));}}}

JDK 8u71 中的实现:

ObjectInputStream.GetFieldfields=s.readFields();...Map<String,Object>streamVals=(Map<String,Object>)fields.get("memberValues",null);...Map<String,Object>mv=newLinkedHashMap<>();for(Map.Entry<String,Object>memberValue:streamVals.entrySet()){// ▶ 遍历仍在Stringname=memberValue.getKey();Objectvalue=null;Class<?>memberType=memberTypes.get(name);if(memberType!=null){value=memberValue.getValue();if(!(memberType.isInstance(value)||valueinstanceofExceptionProxy)){value=newAnnotationTypeMismatchExceptionProxy(...)...;}}mv.put(name,value);}UnsafeAccessor.setMemberValues(this,mv);

区别不只是少了一行setValue(...)

旧版readObject直接在攻击者可控的memberValues上原地修值;新版readObject把成员值先复制进一张新建的LinkedHashMap,最后通过Unsafe写回字段。

写时触发链所依赖的那次自动setValue(...)调用,自此消失。

这次重写到底拦了什么、没拦什么?

它拦的是: readObject() 阶段那次"原地修值"的自动调用(setValue 已移除) 它没拦的是: Commons Collections 包里的任何类—— LazyMap、ChainedTransformer、InvokerTransformer 均未受影响 它留下的是: streamVals.entrySet() 这次遍历仍以某种形式存在, 但已经只是 JDK 内部类的实现细节

对应出处:

  • AnnotationInvocationHandler.java(jdk7u21-b11)
  • AnnotationInvocationHandler.java(jdk8u71-b15)

3. 链上还保留着触发、串联和执行能力

入口被限制,不等于整条链子都失效了。

JDK 修改的是自己包里的一个内部类,Commons Collections 包中的组件并未受影响,CC1 积累下来的能力大部分仍然成立:

承担的能力
LazyMap缺失 key 时把get()接进transform()(触发能力)
ChainedTransformer把多个Transformer串成一条调用链(串联能力)
ConstantTransformer给链一个固定起点
InvokerTransformer单步方法调用,后半段执行手段

后半段原样可用:

LazyMap.get(key) -> ChainedTransformer.transform(key) -> ConstantTransformer(Runtime.class) -> InvokerTransformer("getMethod") -> InvokerTransformer("invoke") -> InvokerTransformer("exec")

LazyMap.get()Runtime.exec()这一整段都不需要重新分析。

只需要补齐自动调用的入口,就可以把链串起来。

4. 链的前半段缺少把调用接进可控 Map 的入口能力

反序列化阶段,缺的是一次自动发生的方法调用:作用在攻击者可控的对象上,并最终落到LazyMap.get()

缺口可以分成两个半块:

1. 自动调用点:某个 Serializable 类的 readObject() 里, 自动对攻击者可控对象调用一个方法 2. 桥:存在一个对象,把这次方法调用的内部 转成 map.get(key)

CC1 中这两块分别由AnnotationInvocationHandler.readObject()和"动态代理 + 按方法名取值"承担。

第一块已不可靠;第二块本身未被修改,但它是为了承接entrySet()这次特定调用而引入的——调用点发生变化,桥接也需要随之更换。

还有一个更隐蔽的要求。

这次要找的自动调用,不能再是"反序列化之后附加的修正动作"——这类动作一次修补即可删除。

它应当是某个数据结构在重建时不可省略的步骤:删掉它,这个类自身便不再成立。

5. 反序列化期的自动 hashCode 调用成为新的入口方向

沿"不可省略的步骤"往下找,候选范围收窄到容器类。

哈希容器在readObject()中的工作,本质是把流中的 key-value 重新摆回桶中。

摆放位置由 key 的哈希值决定,因此对每个 key 调用一次hashCode(),不是附加动作,而是重建哈希表的必经步骤。

只要这个类仍是哈希表,这一步就在,任何版本都无法删除。

HashMap正是如此。JDK 8 中它的readObject()末尾:

for(inti=0;i<mappings;i++){Kkey=(K)s.readObject();Vvalue=(V)s.readObject();putVal(hash(key),key,value,false,false);}

hash(key)的第一步就是调用key.hashCode()

staticfinalinthash(Objectkey){inth;return(key==null)?0:(h=key.hashCode())^(h>>>16);}

JDK 7 写法不同,动作相同:putForCreate()中先计算hash(key.hashCode())

版本之间实现细节屡有变化,这次调用始终存在。

这一判断可以逐版本核实:JDK 17 中的hash()putVal()和 JDK 8 完全一致,HashMapHashSet自 JDK 1.2 引入,重算哈希是重建自身的固有步骤,不存在被修补删除的理由。

入口一侧,CC6 不受 JDK 版本限制。

第一块支点至此确定:HashMap这类反序列化时会重建哈希结构的容器。

尚缺第二块:一个hashCode()内部自带map.get(key)的对象,把这次哈希计算桥接进LazyMap

对应出处:

  • HashMap.java(jdk8u)
  • HashMap.java(jdk17u)

6. 替代支点开始收敛出来

先补第二块:把hashCode()桥接到map.get(key)[读时触发链LazyMap.get()]的对象。

符合这个条件的是org.apache.commons.collections.keyvalue.TiedMapEntry:它把一张Map和一个 key 绑在一起,充当这张表的一个条目。关键在两处方法:

publicObjectgetValue(){returnmap.get(key);}publicinthashCode(){Objectvalue=getValue();return(getKey()==null?0:getKey().hashCode())^(value==null?0:value.hashCode());}

hashCode()为了计算条目哈希,需要先取得条目的值;取值的方式就是map.get(key)

TiedMapEntry.hashCode() -> getValue() -> map.get(key) // map 换成 LazyMap,就是 LazyMap.get(key)

构造时将LazyMap和一个不存在的 key 绑入,之后任何一次对该条目的哈希计算,都会进入LazyMap的懒加载分支,ChainedTransformer被带起。

此外,TiedMapEntryequals()toString()内部同样调用getValue(),可桥接的调用不止hashCode()一种;CC5 走的是toString()一路。

出处:commons-collections-3.2.1-sources.jar中的TiedMapEntry.java

第一块支点上一节已经确定。ysoserial 在HashMap外再包一层HashSetHashSet本身不存数据,内部委托给一张HashMap,其readObject()逐个读出元素后执行map.put(e, PRESENT),同样落到HashMap.put() -> hash(key) -> key.hashCode()

对应出处:

  • HashSet.java(jdk8u)

两块支点都确定后,拼合时会遇到一个新问题:链在构造时就会先执行一遍。

组装 payload 时,总要执行一步map.add(entry)map.put(entry, ...)。向哈希容器中放入元素,当场就要计算entry.hashCode()——这次计算发生在攻击者自己的机器上,后果有两个:

1. 链提前触发:命令在构造 payload 的进程中被先执行 2. 链在目标侧失效:LazyMap.get() 会把生成的值回填进 innerMap, 反序列化时再 get 同一个 key,命中缓存, 懒加载分支不会再走

因此构造时的核心要求是:组装过程中的任何时刻,都不能真正对TiedMapEntry执行哈希计算。

ysoserial 的做法是把"放入"和"替换"拆成两步:先向HashSet中放入一个无害的占位对象,再通过反射沿内部结构把占位 key 原地换成TiedMapEntry(多版本兼容的 try/catch 分支略):

TiedMapEntryentry=newTiedMapEntry(lazyMap,"foo");HashSetmap=newHashSet(1);// 变量名沿用 ysoserial 原文,类型为 HashSetmap.add("foo");// 先放无害占位 key,String 的 hashCode 与链无关// 反射:HashSet.map -> HashMap.table -> 节点.keyFieldf=HashSet.class.getDeclaredField("map");...HashMapinnimpl=(HashMap)f.get(map);Fieldf2=HashMap.class.getDeclaredField("table");...Object[]array=(Object[])f2.get(innimpl);Objectnode=array[0]==null?array[1]:array[0];FieldkeyField=node.getClass().getDeclaredField("key");...keyField.set(node,entry);// 占位 key 原地换成 TiedMapEntry

整个构造过程中,TiedMapEntry.hashCode()一次也未执行;序列化时HashMap按桶序写出键值,反序列化时重建流程重新对每个 key 计算哈希——链只在目标侧执行。

出处:ysoserial CommonsCollections6.java

另一种常见写法思路相同,只是替换的位置不同:先塞一个空的ChainedTransformerLazyMap,让构造时那次hashCode()空转,序列化前再通过反射设置一个带payload的 transformer 数组,并清除回填进innerMap的 key。

7. CC6 由此重新成立

把入口、桥接和 CC1 留下的后半段接起来,CC6 的完整链路:

不包HashSet、直接使用HashMap.readObject() -> putForCreate / putVal的版本,链路结构一致。

CC1 和 CC6 的前半段对比:

CC6 保留 CC1 的触发、串联和执行能力,把入口从AnnotationInvocationHandler的版本相关行为,换成哈希容器重建结构时对 key 的必然hashCode()调用,再用TiedMapEntry把这次调用桥进LazyMap.get()

http://www.jsqmd.com/news/1290770/

相关文章:

  • 目前全自动评价系统平均速度19.3s
  • 太空算力深度报告摘要:太空算力产业链、全球企业布局、运行模式
  • 2026年江苏保湿抑尘剂厂家移动电话信息汇总 - 品牌排行榜
  • 2026年优质汽车改色店业内推荐参考指南 - 品牌优推
  • 2026年哪家做超级电容器的公司比较好? - 品牌排行榜
  • Python包管理工具pip深度解析:从基础原理到实战排坑指南
  • 掌握 Agent 记忆管理:从入门到进阶,收藏这份大模型面试攻略
  • 小龙虾怎么安装OpenClaw?2026最简单安装流程
  • 6款AI写作辅助平台盘点
  • WASM 标准化路线图:GC、组件模型和 WASI 的成熟时间表解读
  • 2026年南山区搬家公司标准化服务运行逻辑全解析 - 品牌优推
  • 2026深圳明星娱乐营销公司排行|深圳明星微博宣传公司哪家好? - mobible
  • 本体在企业知识中台中的应用:技术原理与落地效果
  • VMware虚拟机安装Windows 2000全攻略:兼容性配置与问题排查
  • AI时代已来临!收藏这份小白转行指南,高薪岗位等你来挑战!
  • 2026年长沙有实力的处理民商事纠纷公司口碑排名 - 品牌排行榜
  • QuickRecorder:macOS上最专业的免费屏幕录制工具终极指南
  • OntoL产品自主推理演示 - 北方的银狐
  • RT-Thread FINSH组件:嵌入式实时系统的交互式调试与运行时管理利器
  • 找滁州雨水检查井批发厂家要注意哪些细节 - 品牌优推
  • STM32 LL库输入捕获:从原理到实战,精准测量脉冲与频率
  • 终极摄像头流媒体解决方案:go2rtc专业配置与实战指南
  • OpenClaw 2.7.9 稳定版搭建指南,45.7MB 整合包解压即用部署流程
  • AI 面试官时代来了:别和算法对背八股,要去讲设计
  • AI合同模板生成不是“填空游戏”:基于278份真实判例训练的语义合规校验引擎首次公开
  • 【AI数字员工】营销团队 6 角色混合编队:人机协同的实战样本
  • 2026南京装修公司真实测评|不靠广告,只看入住口碑与落地保障 - 装修百科
  • 2026年选兰州多路功放汽车改装服务商看这里 - 品牌优推
  • 2026年想找广东评价高的空运DDP集运服务公司看这篇 - 品牌优推
  • 二寸怎么剪成小二寸:表单限制分辨率时先分清类型 - AI测评专家