☰
老漏洞新防线:JBoss反序列化漏洞原理与防御
2026/10/21 6:27:55 网站建设 项目流程

干了这么多年安全评估,我几乎每年都会在内网资产里碰到 JBoss JMXInvokerServlet 反序列化漏洞(CVE-2015-7501)的影子。很多团队以为这是“2015年的老漏洞,早修复了”,但实际上一到老旧系统盘点,总能翻出几台还开着/invoker/JMXInvokerServlet的服务器。这个漏洞能做什么?一句话,攻击者只需要向这个开放接口提交一段精心构造的Java序列化数据,就能像坐在服务器本地上开着终端一样执行任意命令,后续植入后门、反弹会话、横向扩散,都是顺理成章的事。这篇文章适合刚接触安全的新人,也适合维护老旧中间件的运维和应急响应同学,我会从原理讲到检测和防御,把容易被忽略的细节拆开揉碎。

先说结论:这个漏洞能活到今天,核心原因是三个“不匹配”——老版本JBoss默认暴露了危险接口,Java原生的反序列化机制过度信任输入数据,而commons-collections这个基础库又提供了现成的“武器化工具”。三者叠加,才造成了只凭一个HTTP POST请求就能命令执行的局面。下面我从历史背景开始,一步步往下拆。

1. 漏洞背景:为什么JBoss反序列化漏洞至今仍有生命力

1.1 漏洞是什么,一句话讲清楚

JBoss在4.x、5.x、6.x等版本中内置了一个名叫JMXInvokerServlet的HTTP接口,它的作用是让远程客户端通过HTTP调用JBoss内部的JMX MBean方法。为了传递方法参数,这个Servlet在设计与实现上直接采用了Java原生序列化对象作为请求格式,也就是说,客户端把参数对象序列化后POST到接口,服务端再反序列化取出对象并调用对应方法。

问题出在“反序列化之前没有任何过滤”。Java对象的readObject()方法在还原对象时,会按照序列化数据里的类描述和字段描述去执行对应对象的readObject、readResolve等方法。构造特殊的类组合,就能让这些方法在还原过程中反射调用系统命令。这个漏洞正是利用这一点,把“一个正常的JMX调用入口”变成了“一个无认证的远程命令执行入口”。

很多资料把CVE-2015-7501简单写成“JBoss反序列化漏洞”,其实不够准确。更精确地说,它描述的是“JBoss JMXInvokerServlet在处理Java序列化对象时缺少安全限制,可被用于远程代码执行”。漏洞本身不是一个代码漏洞,而是一个“不可信输入被反序列化”的组合问题,因此影响范围极广,修复方式也需要层层叠加才稳妥。

1.2 影响范围与目标画像

从实际暴露面来看,受影响的JBoss版本主要是5.x、6.x以及部分未修复的早期4.x版本。在企业内网里,这类系统通常承担着ERP、OA、报表系统、旧版中间件平台等角色,很难因为一个安全漏洞就直接升级替换,所以长期带病运行的情况非常普遍。

我在评估时整理过一个“高发画像”:目标通常是老旧的Java业务系统,旁路有大而全的管理后台,系统没有加固文档,端口上能看到8080、8443、9999等典型JBoss端口,访问/invoker/路径返回404或500但并未被前置代理完全屏蔽。这类系统一旦接入内网,就很容易被自动化扫描设备发现。

攻击者在整个链条中并不需要任何账号密码,也不需要与目标有既有的信任关系。只要网络层能访问到JBoss的HTTP端口,同时该Servlet路径没有被删除或禁止,利用条件就成立。这也是为什么安全圈一直把这类接口列为高危入口,而不是简单归为“中间件配置问题”。

1.3 和CVE-2015-4852的关系

提到CVE-2015-7501,必然绕不开CVE-2015-4852。这两者经常被混着说,但定位并不一样。CVE-2015-4852描述的是Apache Commons Collections库在反序列化过程中可以被构造为执行任意代码的问题,本质是一个通用库缺陷;而CVE-2015-7501描述的是JBoss JMXInvokerServlet在具体应用这个有缺陷库时,暴露出的可利用场景。

理解了这个关系就会明白,为什么修复不能只升级JBoss版本,也不只是给Servlet打补丁。即便JBoss把入口封掉了,只要JDK里还躺着其他危险反序列化链路,或者业务代码里还有项目自定义的ObjectInputStream.readObject()调用,同样的攻击手法换个入口依然能成立。防御时要始终带着“库、入口、信任边界”三个视角去看。

2. 原理拆解:从HTTP请求到命令执行的完整链路

2.1 Java序列化的“信任危机”

Java原生的序列化机制,简单理解就是把一个对象转换成二进制字节流,再用反序列化把它恢复回对象。这本身是一种非常便利的跨进程通信手段,RMI、JMX、JMS、HTTP Session存储等场景都在用它。但便利背后有个隐含假设:数据来源可信。

反序列化时,ObjectInputStream.readObject()会根据流中的类描述符,加载对应类并调用其readObject()方法。这个“还原过程”不是简单的内存拷贝,它会执行被还原对象自身的部分代码逻辑。如果一个类的readObject()方法实现里调用了别的对象的成员方法,而这些方法和参数又完全由序列化数据控制,攻击者就可以通过精心挑选类组合,把还原过程变成一个“程序执行流水线”。

生活化一点类比:快递包裹上的寄件人信息可以被恶意伪造,而收件人在拆包裹的时候,还会根据包裹里的说明书去启动一台机器——这台机器本身没问题,但说明书被换成了恶意指令。Java反序列化漏洞整体上就是这类“过度信任输入数据”的问题。

2.2 JMXInvokerServlet为什么成了入口

JBoss的JMX架构允许通过HTTP调用MBean方法,暴露出一个适配器端点,就是/invoker/JMXInvokerServlet。它接收的请求体必须是Java序列化后的对象,服务端拿到后先反序列化成Invoker结构,再执行相关MBean的调用。

关键点在于,这个Servlet在默认配置下不强制认证,也不检查请求来源,完全可以被外部网络直接访问。即使某些版本里JBoss自身有安全管理,但如果没有正确配置安全域,接口依然裸奔。攻击者只需向这个路径发送一段精心构造的POST数据,服务端就会在反序列化的瞬间触发恶意行为。

这里有个容易误判的细节:有的目标访问/invoker/JMXInvokerServlet会返回404,但这不代表漏洞不存在。部分部署会把该Servlet挂载到自定义路径下,或者通过URL重写、反向代理映射。真正稳妥的判断方法,是看实际HTTP响应格式以及JBoss部署描述文件里注册的Servlet映射,单纯靠一个路径猜是不行的。

2.3 危险Gadget:commons-collections与InvokerTransformer

有了反序列化入口,还差一个能在反序列化过程中产生“危险副作用”的类。commons-collections是一个被大量Java项目依赖的基础工具库,提供集合操作的便利实现。其中有一个InvokerTransformer类,它在transform()方法里会通过反射调用指定对象的指定方法。

正常情况下,InvokerTransformer只是库里的一个小工具,但攻击者可以把多个Transformer串联起来:先用一个ConstantTransformer返回常对象,再通过InvokerTransformer反射调用Runtime.getRuntime(),紧接着再用一个InvokerTransformer反射调用exec()。一连串调用放在一起,就等价于执行了任意系统命令。

当然,原始的攻击链不会直接把这么简单的调用写在序列化流里,而是要把这条调用链包装成一个类的字段,并让这个字段在readObject()阶段被触发。常见的包装方式包括AnnotationInvocationHandler、PriorityQueue、BadAttributeValueExpException等。所有这些组件拼在一起,才成为一条完整的“gadget chain”。

2.4 利用链是如何被“拼装”出来的

一条反序列化利用链,可以拆成两个部分:入口类和执行链。入口类负责在readObject()时触发一个可控的调用;执行链则负责把一次次方法反射调用串联成命令执行。

简化的调用流程可以表达为:

  • 入口类反序列化时进入readObject()
  • 入口类内部对某个Map结构进行遍历,触发某个Transformer的transform()方法
  • transform()内部通过反射调用下一个Transformer,形成链式调用
  • 链尾执行Runtime.exec()系统命令

实际攻击中还要考虑很多现实因素:目标JDK版本是否限制了某些类、目标类加载器里是否有对应版本库、命令执行是否有回显、是否需要外带数据等。不过这些并不改变根本原理,只是增加了利用工具在生成payload时的分支判断。

需要特别强调的是,InvokerTransformer这类危险类并不是单独存在于某个JBoss版本里,它来自commons-collections库。也就是说,哪怕你换一个Web应用服务器,只要项目依赖了存在缺陷的commons-collections 3.x版本,并且存在反序列化入口,同样可以被利用。这一点在做防御时尤其要警惕。

3. 授权环境实测:利用思路与关键操作记录

以下内容全部在隔离的授权靶场环境中验证,请勿对未授权目标执行任何检测。安全评估的价值在于提前发现风险,而不是制造风险。

3.1 第一步:确认JBoss版本与Servlet路径

碰到一个疑似JBoss系统,我一般不会直接发恶意请求,而是先用低风险方式收集版本指纹。通过HTTP响应头、错误页面、JMX控制台路径、静态资源特征等,能快速判断目标是否为JBoss及大致版本。这一步不需要发送反序列化数据,风险低,但能大幅提高后续判断准确率。

确认版本后,再检查目标是否暴露了关键路径。JBoss各版本中,/invoker/JMXInvokerServlet是最常出现的路径,但也存在/invoker/EJBInvokerServlet等同类接口。直接访问这些路径时,正常配置下往往返回HTTP 405或500,因为缺少合法的序列化请求体;如果返回405“Method Not Allowed”或“Not Found”之外的异常页面,就需要再结合响应头判断。

我习惯用且仅用“发送一个完全无害的空POST请求”来确认Servlet是否存活。请求体为空时,JBoss会尝试反序列化并抛出异常;不同版本抛出的异常形态不同。从异常信息类型可以进一步确认版本与Servlet行为,这比单纯看路径返回码要可靠得多。整个过程不产生命令执行,适合在授权评估初期使用。

3.2 第二步:生成并发送序列化探测对象

确认目标存在反序列化入口后,下一步是生成一个“可观察但无破坏性”的探测数据。业界常见的思路是构造一个能触发延时命令的payload,例如让目标执行一条sleep命令,通过响应时间差判断漏洞是否可利用。这样做不会对业务系统造成持久性影响,也能避免在目标主机上留下明显攻击痕迹。

利用工具方面,有多个开源的Java反序列化payload生成器可用。它们会提供不同gadget链模板,比如基于CommonsCollections1、CommonsCollections5、Jdk7u21等。选择哪条链,主要取决于目标JDK版本、commons-collections库的版本和是否开启某些安全限制。经验上,先使用兼容性最广的CommonsCollections1尝试,如果失败,再换其他链,成功率会更高。

发送时,需要把生成的二进制对象数据作为HTTP请求体直接POST到/invoker/JMXInvokerServlet,同时设置Content-Type为application/x-java-serialized-object。这里要留意:不是所有工具生成的数据都适合直接发送,个别工具输出的是Base64编码文本,需要先解码成原始字节再POST,否则服务端在首字节解析时就会失败。

3.3 第三步:判断执行结果与绕过WAF的思路

JBoss的反序列化漏洞大多没有回显,也就是说,命令执行成功也不会把标准输出返回到HTTP响应里。判断是否命中,常用的方式有三种:

  • 延时判断:构造执行sleep 5等命令,观察响应时间是否明显增加;
  • DNS外带:构造执行DNS查询命令,借助外部DNS日志确认目标是否真的执行了命令;
  • 写入判断:构造命令在Web目录写入一个静态标记文件,随后访问这个文件确认结果。

这三种方式在授权测试中各有优劣。延时判断最简单,但容易受网络抖动影响;DNS外带需要提前准备外部域名记录,对封闭内网环境不一定可行;写入文件最直观,但会触发文件监测告警,实战中要视情况选择。

如果目标前面存在WAF或入侵检测设备,常见绕过思路是对payload进行混淆或编码。比较初级的WAF会直接匹配InvokerTransformer、Runtime等关键字,因此实际投递的序列化流往往不是明文类名字符串,而是经过各种混淆类加载器包装后的形态。但从防御角度看,不能只靠匹配关键字,需要结合长度、入口路径、请求体首字节等多个特征综合判断。

某些目标还会针对性地封禁源IP,遇到这种情况,首先确认是否因为长时间高频探测被封,然后调整检测频率或模拟真实浏览器行为。安全测试的价值在于验证风险,不在于穷举绕过手法,尽可能降低对目标的影响才是基本原则。

4. 防御、检测与应急响应清单

4.1 临时止损与永久修复的正确顺序

如果确认JMXInvokerServlet存在漏洞,又没法立即下线系统,我建议按“先切流量—再补配置—最后升组件”的顺序处理。优先通过防火墙或负载均衡策略,对所有来源IP封禁指向/invoker/JMXInvokerServlet的POST请求;业务如果确实需要使用JMX接口,就改为通过内网专用管理网段访问,并增加来源IP白名单。

临时措施之后,尽快在应用层部署描述里删除或注释掉这个Servlet的映射。不同JBoss版本放置的位置略有差异,但思路一致:找到部署目录里的jboss-web.xml、web.xml或jboss-service.xml中与JMXInvokerServlet相关的配置项,将<servlet-mapping>注释掉,并删除指向该Servlet的<servlet>声明。修改后必须重启服务,并重新测试路径是否已经无法访问。

永久修复的根基是升级JBoss到已修复版本,同时升级commons-collections相关依赖。注意,升级不是改个版本号那么简单,需要做回归测试,因为JBoss和业务项目依赖的库版本之间往往存在兼容约束。有的老系统升级中间件后,业务代码里的类加载逻辑会出问题,因此要在预发布环境完整跑一遍业务回归。

4.2 中间件层加固与组件升级

除了删Servlet,中间件加固还要完成几件事:关闭不需要的JMX远程调用端口,禁止任意MBean访问,对JMX控制台加认证。JBoss默认自带jmx-console和web-console,它们同样是危险面。如果业务用不到,直接删除对应war包;必须使用的话,设置在管理网段内,并配置强密码。

组件层面,需要梳理项目依赖的 jar 包,重点排查commons-collections等历史库是否还停留在受影响的3.x版本。许多开发团队只关注业务代码自身的安全,对间接依赖不了解,这是反序列化漏洞长期存在的重要原因。建议建立依赖清单,定期用依赖检查工具扫描,把存在已知漏洞的库列为禁用品,推动开发侧替换方案。

对于无法立刻替换的老系统,可以考虑引入反序列化过滤组件,比如在ObjectInputStream上增加类白名单/黑名单过滤。这类方案的效果取决于过滤名单是否完整,不能依赖单一规则,但作为纵深防御的一层是完全有必要的。更高版本的JDK自带了ObjectInputFilter机制,老版本JDK也可以借助字节码增强方式实现类似过滤。

4.3 流量层检测特征与规则落地

检测这个漏洞,核心是发现两个特征:特定HTTP入口,以及特定反序列化数据。先说入口特征,正常业务不会频繁POST到/invoker/JMXInvokerServlet,这条路径本身就是一个强告警信号。IDS、WAF、流量分析平台都可以针对这个路径设置高优先级规则。

再看数据特征,Java原生序列化流的十六进制开头是固定的ac ed 00 05,对应Base64编码则常以rO0AB开头。检测规则可以先从请求体前几个字节做快速判断。如果请求体不是以这个魔数开头,基本可以排除Java原生序列化攻击;如果命中,再进入深度内容检测,匹配gadget链中的关键类名,比如InvokerTransformer、TransformedMap、PriorityQueue等。

实际部署规则时要注意误报问题。有些通过JMX做正常管理的程序也会发送序列化数据,尤其是管理端到管理端的调用。建议把入口特征和内容特征加权组合,例如:URI含JMXInvokerServlet且Content-Type为application/x-java-serialized-object且请求体超过1KB,触发告警;再结合是否命中危险类名提升告警级别。这样既能覆盖主要攻击方式,又不会一天到晚误报。

4.4 日志审计与事件处置清单

一旦发现疑似攻击行为,第一件事是回到日志里定位时间线。JBoss的访问日志里可以搜索JMXInvokerServlet,确认攻击源IP、请求时间、请求体大小、UA字段。正常情况下POST这个接口的请求体不会很大,如果有几十KB甚至几百KB的POST数据,就属于明显异常。

应急响应时,我习惯按以下清单推进:

  • 收集访问日志和中间件日志,圈定最早攻击请求;
  • 从流量设备上提取该IP在关键时间窗内的全部TCP会话,补充证据;
  • 检查Java进程状态,确认是否有异常外联、监听端口变化;
  • 检查JBoss部署目录和Web目录,确认是否新增了war包、jsp文件或隐藏文件;
  • 检查系统层任务计划、自启动项以及是否存在异常计划任务;
  • 如发现可疑进程,用内存取证工具dump分析,确认是否被植入内存马。

这个漏洞被利用后,攻击者通常会做三件事:建立新的持久化路径、尝试读取配置文件中的数据库口令、向内网其他主机扩散。处置时不要只把恶意进程杀掉,而是要继续回溯攻击者在内网的横向路径,防止遗漏持久化后门。切忌直接重启服务器导致内存中恶意数据丢失,先做证据固定再恢复系统。

5. 安全评估中的经验手记

5.1 几个容易踩的坑

我在实际项目里踩过不少坑,先说最有代表性的三个。

第一,看到HTTP响应头里有JBoss标志就以为漏洞确定存在,这只是指纹信息,还需要确认具体Servlet路径是否真的对外可访问。有一次我碰到一台旧JBoss,路径确实存在,但前面Nginx做了URL白名单,业务侧完全访问不到/invoker/目录,这种场景不能直接判为可利用,只能算“配置风险”。

第二,忽略JDK版本对利用链的影响。同一套commons-collections库在不同JDK上稳定性差别很大,尤其是高版本JDK对反序列化的类加载限制越来越严。有些检测报告只写了“存在反序列化漏洞”,但没有验证当前JDK下的可利用性,这会让甲方误判风险等级。评估时最好把JDK版本和利用链测试结果一起记录。

第三,过度依赖特征检测而忽略业务可用性。有些防御团队为了防这个漏洞,直接把ac ed 00 05开头的请求全部拦截,结果业务方通过Java客户端正常调用JMX管理功能时全部失败,白天出现大面积故障。反序列化防御一定要预留业务白名单通道,而不是一刀切。

5.2 我现在的固定检查流程

现在每次评估老系统,我都会走一条固定流程,既稳妥又节省时间。

先看端口和指纹,确定JBoss版本;再看/invoker/路径下的Servlet是否可访问;然后通过空POST观察异常响应,确认接口是否在处理反序列化数据;接着在授权前提下用延时型探测验证利用链;同时同步检查依赖的commons-collections版本;最后给出包含“修复优先级、临时缓解措施、长期加固建议”三层的报告。

如果时间紧张,我会至少完成路径可访问性判断和依赖版本检查这两步。这两步不需要发送任何恶意payload,就能在绝大多数情况下判断目标是否存在CVE-2015-7501的暴露条件。剩下的是验证深度的问题,而不是“有没有问题”的问题。

最后分享一个个人习惯:针对这类反序列化漏洞,我每次都会在报告里专门写一段“如果零日利用链出现,现有的检测规则会不会失效”,并建议运维团队保留网络层全流量记录。因为反序列化漏洞的利用方式更新速度非常快,单点规则永远只能防住已知攻击,只有留下完整流量日志,才能在发生未知攻击时做回溯分析。这个习惯帮我处理过不少棘手的应急事件,也推荐给所有负责Java应用安全的人。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询