Blog

CVE-2026-34486: how a patch opened a new hole in Apache Tomcat

CVE-2026-34486-apache-tomcat
Apache / CVE / Security

CVE-2026-34486: how a patch opened a new hole in Apache Tomcat

Picture this: you got a vulnerability notice for Apache Tomcat, quickly updated to 9.0.116, closed the ticket, and breathed a sigh of relief. Then a month later it turned out that exact patch had created a new hole — and a worse one than the original.

CVE-2026-34486 is a textbook regression: developers were fixing CVE-2026-29146, a padding oracle attack against Tomcat Tribes, but introduced a logic error that opened the door to unauthenticated RCE via deserialization. Apache rated it Important, NVD assigned CVSS 7.5 — and this is one of those cases where the number undersells the real danger: the previous vulnerability in the same component took hours to exploit, this one takes a single packet. Affected versions are 9.0.116, 10.1.53, and 11.0.20. A PoC was published by researcher Bartlomiej Dmitruk from striga.ai.

The patch landed on April 9, 2026 — commit 776e12b3 in the official repository. If you updated specifically to close CVE-2026-29146 and landed on 9.0.116, you’re still vulnerable. You need 9.0.117.

WHAT IS APACHE TOMCAT TRIBES

Apache Tribes is the internal message bus for Tomcat clusters. When you have multiple instances behind a load balancer, they need to synchronize session state — otherwise a user hitting a different node after a failover gets logged out. Tribes handles exactly that: it passes messages between cluster nodes over a dedicated port (4000 by default) that isn’t exposed outside the perimeter.

Sounds like a sealed internal channel — which is exactly why its security tends to get less attention than the web interface. That’s a mistake. There are three realistic paths to Tribes port: compromise any host on the corporate network, find the least-hardened server in the cluster itself and use it as a foothold, or — more common than you’d think — SSRF: if the web application makes HTTP requests to user-supplied URLs, an attacker simply points it at the internal address and port 4000.

HOW THE BUG WORKS

The original CVE-2026-29146 problem was that EncryptInterceptor used CBC encryption without message authentication. That’s a classic padding oracle: by intercepting encrypted packets and watching how the server reacts to padding errors, an attacker could methodically reconstruct the plaintext. Tedious, but it works.

When fixing it, developers reworked the messageReceived() method in EncryptInterceptor — but moved the super.messageReceived() call outside the try/catch block. Here’s what the vulnerable code looks like:

@Override
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(sm.getString("encryptInterceptor.decrypt.failed"), gse);
    }
    // Deserialization runs regardless — even after a decryption failure
    super.messageReceived(msg);
}

The logic is fundamentally broken. If an attacker sends raw unencrypted data, the decrypt() call throws BadPaddingException or IllegalBlockSizeException. The exception gets caught, the error goes to the log — and execution continues. The super.messageReceived(msg) call sits outside the try block, so it runs regardless, passing the attacker’s original unencrypted data down the stack.

From there the data reaches GroupChannel, which hands it to XByteBuffer.deserialize(). That’s Java’s standard ObjectInputStream — which means deserialization automatically invokes methods like readObject() and hashCode() on any objects described in the incoming payload.

HOW IT IS EXPLOITED

Java deserialization isn’t dangerous by itself — what’s dangerous is whatever classes are on the server’s classpath. Apache Commons Collections 3.x is a near-universal dependency for Java applications, and its gadget chains leading to arbitrary code execution have been well-documented for years.

The call chain using Commons Collections looks like this:

HashSet.readObject()
  → HashMap.put(key, ...)
    → TiedMapEntry.hashCode()
      → LazyMap.get("poc")
        → ChainedTransformer.transform()
          → ConstantTransformer → Runtime.class
          → InvokerTransformer → Runtime.getMethod("getRuntime")
          → InvokerTransformer → Runtime.getRuntime()
          → InvokerTransformer → runtime.exec(cmd)

ysoserial ships eight ready-made variations for Commons Collections covering different library versions, plus chains for Spring, Groovy, Commons BeanUtils, and built-in JDK classes. The attacker’s job is to find the right chain for the target environment and send one packet to the Tribes port.

Compare that to the original CVE-2026-29146: intercepting traffic, probing the server methodically, analysing padding error responses. Here it’s one request, no authentication, no prior knowledge of the encryption key needed.

REAL-WORLD ATTACK CHAIN

The attacker is inside the corporate network — via a compromised employee machine, an unprotected Wi-Fi segment, or a foothold on a neighbouring cluster node. The alternative entry point is SSRF on the web application: if the app makes HTTP requests to user-supplied URLs, the attacker simply directs it at the internal address and port 4000. The Tribes port isn’t listening externally, but from inside it’s reachable.

From there it’s straightforward. The attacker identifies the Tomcat version via HTTP headers or an error page, confirms that Tribes is active (a cluster setup means a <Cluster> element in server.xml), generates a payload with ysoserial using the right gadget chain for the application’s dependencies — and sends one raw packet to port 4000. No authentication. No encryption key. The server itself invokes the right methods during deserialization.

The outcome depends on how Tomcat is running. Without systemd isolation it’s full server compromise in a single packet, running as the Tomcat process user. With PrivateTmp, NoNewPrivileges, and capability restrictions in place the blast radius is contained to the process — but the RCE still happened, and the attacker is already inside.

TIMELINE

The original CVE-2026-29146 — the padding oracle in EncryptInterceptor — was fixed in versions 9.0.116, 10.1.53, and 11.0.20. Those are the exact versions where commit 0112ed22 appeared, moving super.messageReceived() outside the try/catch block and creating the new problem in the process.

On March 26, 2026, shortly after the patch shipped, researcher Bartlomiej Dmitruk from striga.ai reported the regression to the Apache Tomcat security team. The timing suggests a deliberate audit of the patch rather than an accidental discovery — someone sat down and read the diff.

On April 9, 2026, CVE-2026-34486 was published. Fixed versions 9.0.117, 10.1.54, and 11.0.21 shipped the same day with commit 776e12b3, restoring correct error handling in messageReceived(). A public PoC from striga.ai followed shortly after disclosure.

WHY IT MATTERS

Apache Tomcat is one of the most widely deployed Java servers in the world — banks, payment processors, enterprise portals. Clustering with Tribes is used wherever horizontal scalability is needed, which is exactly where the load and data sensitivity are highest.

What makes this worse is that many teams may think they’re covered — they updated to 9.0.116 specifically for CVE-2026-29146. They’re not. That creates a false sense of security that’s arguably worse than having no patch at all: at least unpatched teams know they’re exposed.

There’s also a broader lesson here about regression vulnerabilities. A security fix applied without fully tracing the execution path can produce something worse than the original bug. CVE-2026-29146 required traffic interception and methodical probing. CVE-2026-34486 requires one unauthenticated packet.

WHAT TO DO

First, find out what version you’re actually running. If Tomcat was installed through a package manager, the version isn’t always obvious. Check it like this:

find / -name "catalina.jar" 2>/dev/null | xargs -I{} sh -c 'unzip -p "{}" org/apache/catalina/util/ServerInfo.properties 2>/dev/null | grep server.number'

Or via HTTP if the status page is enabled:

curl -s http://localhost:8080/ | grep -i tomcat

Then update. There’s a non-obvious catch here: Debian/Ubuntu repositories often lag several minor versions behind upstream. Running apt install tomcat9 might not give you 9.0.117 — check first:

apt-cache policy tomcat9

Look at the Candidate: line. If it’s not 9.0.117 or newer, update manually from the official download page (tomcat.apache.org/download-90.cgi for the 9.x branch): stop the instance, replace bin/ and lib/ while leaving webapps/ and conf/ untouched, restart. If the repository is current:

apt update && apt install tomcat9

After updating, confirm the version actually changed:

sh /path/to/tomcat/bin/version.sh | grep "Server version"

It should show 9.0.117, 10.1.54, or 11.0.21 depending on your branch. If the version hasn’t changed, the update didn’t take.

If you can’t update right now — at least isolate the port. First confirm Tribes is actually active:

grep -n "Cluster" /opt/tomcat/conf/server.xml
ss -tlnp | grep 4000

If the port is listening, close it via nftables while allowing only trusted cluster nodes through:

nft add rule inet filter input tcp dport 4000 ip saddr != { 10.0.1.10, 10.0.1.11 } drop

This is a temporary measure, not a substitute for patching. If Tribes isn’t used at all, make sure the <Cluster> element is commented out or absent from server.xml and disable it.

CONCLUSIONS

CVE-2026-34486 is a case where the cure was worse than the disease. Developers fixed a padding oracle, but a logic error in the execution flow produced an unauthenticated RCE vulnerability instead. The affected versions are exactly the ones that were supposed to be safe after the previous patch.

Check your Tomcat version now. If you’re on 9.0.116, update to 9.0.117. For the 10.x and 11.x branches: 10.1.53 → 10.1.54, 11.0.20 → 11.0.21. This can’t wait for the next maintenance window if the Tribes port is reachable from anywhere inside your network.

Leave your thought here

Your email address will not be published. Required fields are marked *