JSTGTECH
← Back to blog

Java deserialization after Log4Shell: CVE-2023-46604

4 min read

Log4Shell (CVE-2021-44228) made “Java deserialization” a household phrase for a few terrifying weeks in December 2021, but it wasn’t a one-off. Less than two years later, CVE-2023-46604, an unauthenticated remote code execution bug in Apache ActiveMQ’s OpenWire protocol, gave attackers the same outcome — arbitrary command execution with zero credentials — and it wasn’t a lookup-and-log JNDI trick this time, it was textbook insecure deserialization in a message broker that sits on the network path of a huge number of Java shops. CISA added it to the Known Exploited Vulnerabilities catalog within days, and ransomware crews had it weaponized before most defenders had patched (Rapid7). If you assumed Log4Shell was the last time a Java serialization bug would take down a fleet of production systems overnight, this one is worth sitting with.

Root cause

ActiveMQ’s OpenWire is the binary wire protocol brokers and clients use to talk to each other, and by default it’s listening on port 61616 with no authentication required to establish a connection. Inside that protocol, one command type — ExceptionResponse — carries a class name and a message string so a broker can tell a client “here’s the exception that happened.” The marshalling code that unpacks it, BaseDataStreamMarshaller.createThrowable, takes that attacker-supplied class name off the wire and instantiates it directly, passing the attacker-supplied message string into the constructor, without first checking that the class is actually a Throwable (Rapid7).

That’s the whole bug. Anyone who can open a TCP connection to the OpenWire port can send a crafted EXCEPTION_RESPONSE packet naming any class on the server’s classpath — including Spring’s ClassPathXmlApplicationContext, which happily fetches and executes an XML bean definition from an attacker-controlled URL. Point that at a remote XML file that defines a ProcessBuilder-backed bean and the broker runs your shell command. It’s the same family as the Commons Collections gadget chains that made Java deserialization famous a decade ago — take a class that legitimately does something dangerous when instantiated or invoked, and abuse a deserializer that doesn’t discriminate about what it’s allowed to construct. The fix Apache shipped adds exactly the type check that should have been there from the start: reject any “exception” class name that isn’t actually a Throwable before instantiating it (Rapid7).

Blast radius

CVE-2023-46604 scores CVSS 9.8, and researchers rated exploitation complexity as trivial — no auth, no user interaction, one crafted packet (Huntress). The result is arbitrary command execution as whatever OS user runs the broker process, and in the wild that turned into exactly what you’d expect once a reliable pre-auth RCE with a public PoC exists: a race between opportunistic and targeted actors. Rapid7 tracked exploitation attributed to HelloKitty ransomware starting within days of disclosure (Rapid7), Trend Micro and Sekoia both documented the Kinsing cryptomining botnet using it to drop miners and rootkits on Linux hosts (Trend Micro, Sekoia), and SOC Prime and others tracked TellYouThePass ransomware riding the same bug (SOCRadar). Message brokers tend to sit deep in the architecture — application servers, integration layers, IoT backends all talk to them — so a broker compromise isn’t an edge-of-network incident, it’s a foothold with a direct line to whatever internal services trust that broker’s traffic.

Remediation

Apache patched the marshalling logic and shipped fixed releases across every supported branch: 5.15.16, 5.16.7, 5.17.6, and 5.18.3, with 6.0.0 also carrying the fix (Apache ActiveMQ). If you’re still running an unpatched broker, upgrading is non-negotiable — this isn’t a “add authentication in front of it” situation, because the flaw is in how the broker parses its own wire protocol before any application-level auth applies. Restricting network access to port 61616 to only trusted broker and client hosts is a reasonable stopgap while you schedule the upgrade, but given how quickly this was weaponized, treat any internet-facing or broadly-reachable OpenWire port as already assumed-compromised and hunt for follow-on cryptominer or webshell activity, not just confirm the patch applied.

The bigger lesson

What’s actually changed since Log4Shell is the plumbing around detection: WAF and IDS vendors now ship signatures for OpenWire-style deserialization payloads within hours of a PoC landing, CISA’s KEV catalog gives defenders a authoritative “this is being exploited right now” signal instead of relying on vendor advisories alone, and software composition analysis tools flag vulnerable broker and library versions in CI before they ever ship. On the JDK side, JEP 415’s context-specific deserialization filters (Java 17+) let you scope an allowlist to a specific ObjectInputStream instead of one JVM-wide filter that’s either too loose or breaks half your app (Baeldung) — a real improvement over JEP 290’s blunt instrument.

What hasn’t changed is the underlying pattern: a component trusts a class name or a byte stream from the network more than it should, and something reachable on the classpath turns that trust into code execution. A 2024 NDSS study found over 3,600 GitHub Java projects still carrying known deserialization vulnerabilities (Java Code Geeks) — the detection tooling is faster, but it’s still catching the same class of bug, not preventing it from being written. If your Java services still do implicit ObjectInputStream deserialization or unauthenticated binary protocol parsing anywhere on the network path, Log4Shell and ActiveMQ are both telling you the same thing: the fix belongs in the code that trusts the bytes, not in the WAF rule that flags them after the fact.

Related posts