CVE-2024-32030 — Kafka UI Unauthenticated JRMP Deserialization RCE¶
Published 2026-06-06
Unauthenticated remote code execution
An unauthenticated attacker can execute arbitrary commands inside the Kafka UI JVM by registering a cluster whose JMX metrics endpoint points at an attacker-controlled JRMP listener. No credentials or prior access are required.
| Property | Value |
|---|---|
| Project | Kafka UI |
| Affected component | kafka-ui-api — JMX metrics collector (JmxMetricsRetriever) |
| Severity | HIGH |
| CVSS | 8.1 (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| CWE | CWE-502, CWE-94 |
| Affected versions | all versions ≤ v0.7.1 |
| Fixed version | v0.7.2 (patch commit 83b5a60, PR #4427, released 2024-04-10) |
| Advisory | GHSL-2023-230 |
1. Vulnerability Overview¶
Kafka UI (provectus/kafka-ui) is an open-source Spring Boot web application for managing Apache Kafka clusters. When the dynamic configuration feature is enabled (the official README recommends passing -Ddynamic.config.enabled=true), the HTTP API accepts unauthenticated PUT /api/config requests that register new Kafka clusters, including a metrics block specifying a JMX host and port. Kafka UI's JMX metrics collector then connects to that host and port using a standard javax.management.remote.JMXConnector over the Java RMI Message Protocol (JRMP).
JRMP deserializes the server's response before authentication completes. With commons-collections:3.2.2 and scala-library:2.13.9 both present on the classpath as transitive dependencies, an attacker can construct a two-stage deserialization attack that achieves full remote code execution inside the Kafka UI JVM.
Root cause¶
File: kafka-ui-api/pom.xml
The kafka-json-schema-serializer dependency pulled in commons-collections:3.2.2 as a transitive artifact. Commons Collections 3.2.2 added a guard against unsafe functor deserialization, controlled by the system property org.apache.commons.collections.enableUnsafeSerialization. However, scala-library:2.13.9 (also a transitive dependency) provided the building blocks to set that property from a deserialization gadget itself.
Stage 1 — bypassing the CC 3.2.2 guard via a Scala gadget chain:
ConcurrentSkipListMap.readObject() calls comparator.compare() on deserialized keys. By setting the comparator to scala.math.Ordering$IterableOrdering and the keys to scala.collection.View$Fill instances wrapping a SerializedLambda pointing at scala.sys.SystemProperties.$anonfun$addOne$1, the deserialization chain calls System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "true"), disabling the guard.
Stage 2 — arbitrary command execution via CommonsCollections7:
With the guard disabled, a second JRMP connection delivers the standard CommonsCollections7 gadget: Hashtable → LazyMap → ChainedTransformer → InvokerTransformer → Runtime.exec(...).
The fix (commit 83b5a60) excludes commons-collections:3.x from the kafka-json-schema-serializer dependency tree and replaces it with commons-collections4:4.4, whose functor classes do not implement Serializable. This makes the entire CC gadget chain structurally unreachable:
--- a/kafka-ui-api/pom.xml
+++ b/kafka-ui-api/pom.xml
@@ -81,6 +81,12 @@
<groupId>io.confluent</groupId>
<artifactId>kafka-json-schema-serializer</artifactId>
<version>${confluent.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>commons-collections</groupId>
+ <artifactId>commons-collections</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
...
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-collections4</artifactId>
+ <version>4.4</version>
+ </dependency>
2. Vulnerable Environment¶
The reproduction environment is a single Docker container running the official provectuslabs/kafka-ui:v0.7.1 image. That image ships kafka-ui-api.jar with the vulnerable classpath intact: both commons-collections-3.2.2.jar and scala-library-2.13.9.jar are bundled inside the JAR's nested library directory. The dynamic configuration API is enabled via an environment variable, making the unauthenticated cluster-registration surface available immediately.
No real Kafka broker is required at startup. The exploit starts an attacker-controlled broker separately; the Kafka UI API accepts any value for bootstrapServers without contacting it before JMX collection runs.
Download the environment files: env.zip — or browse the individual file: env/docker-compose.yml.
Bring up the environment¶
The service is exposed at http://127.0.0.1:8080. The container name is cve-2024-32030-kafka-ui, and the JVM runs as service user kafkaui (pid 1).
Confirming the vulnerable substrate¶
Run these checks after startup to verify you have the unpatched version:
# 1. API is up
curl -s http://127.0.0.1:8080/actuator/health
# expect: {"status":"UP",...}
# 2. Auth is disabled, dynamic config is active
curl -s http://127.0.0.1:8080/api/config | grep -o '"type":"DISABLED"'
# expect: "type":"DISABLED"
# 3. JVM service user and vulnerable classpath
docker exec cve-2024-32030-kafka-ui ps -o user,args -p 1
docker exec cve-2024-32030-kafka-ui sh -c 'unzip -l /kafka-ui-api.jar | grep -E "commons-collections-3.2.2|scala-library-2.13"'
# expect: user kafkaui; both commons-collections-3.2.2.jar and scala-library-2.13.x.jar listed
The host.docker.internal alias is wired into the container via extra_hosts: host.docker.internal:host-gateway, which lets the Kafka UI JVM open outbound JRMP connections to a listener running on the host.
3. How to Exploit¶
Download the exploit files: exploit.zip — or browse individual files: exploit/run.sh, exploit/JrmpExploit.java.
A single orchestrating script, exploit/run.sh, drives the entire attack. It compiles the JRMP listener, starts an attacker Kafka broker, registers the malicious cluster config, and tears down after itself.
Prerequisites¶
- The vulnerable Kafka UI container is running (see §2).
- Docker is available on the host (the script uses it to compile and run the listener).
- Host TCP ports
1718(JRMP listener) and9092(attacker Kafka broker) are free.
Run the exploit¶
Generate a fresh per-run nonce and invoke the script:
| Argument | Value | Purpose |
|---|---|---|
target_url |
http://127.0.0.1:8080 |
Kafka UI HTTP API (unauthenticated) |
jrmp_port |
1718 |
Host port for the JRMP listener |
marker_path |
/tmp/pwned |
Prefix of the marker file path; the exploit writes /tmp/pwned.<nonce> |
nonce |
poc<epoch><rand> |
Per-run nonce — prevents a stale or planted file from satisfying the check |
What the script does, step by step¶
- Compiles
exploit/JrmpExploit.javainside aneclipse-temurin:17-jdkcontainer against the target's ownscala-library-2.13.9.jarandcommons-collections-3.2.2.jar(bundled inexploit/libs/). - Starts an attacker Kafka broker (
apache/kafka:3.7.0, containercve-2024-32030-poc-broker) advertisinghost.docker.internal:9092, so Kafka UI's admin client can discover a broker node before triggering JMX collection. - Starts the JRMP listener on host port
1718. - Issues
PUT /api/config, an unauthenticated request that registers a cluster whosemetricsblock (type: JMX,host: host.docker.internal,port: 1718) directs the JVM's JMX client at the attacker listener. - Waits through Kafka UI's 30-second scheduled metrics-collection cycles. The first JMX connection receives Stage 1 (the Scala gadget sets the bypass property); the second receives Stage 2 (the CC7 gadget calls
Runtime.exec("touch /tmp/pwned.<NONCE>")). - Tears down the listener and broker containers on exit via a shell trap.
Allow up to approximately two minutes for both scheduler cycles to complete.
Confirming execution — the independent proof¶
Verify success through a channel independent of the exploit script's own output: a direct shell into the Kafka UI container. Check that the marker file was created by the Kafka UI JVM:
docker exec cve-2024-32030-kafka-ui stat -c '%U %n' /tmp/pwned.<NONCE>
# expect: kafkaui /tmp/pwned.<NONCE>
In the verified reproduction run:
docker exec cve-2024-32030-kafka-ui ls -l /tmp/pwned.poc178039439726378
-> -rw-r--r-- 1 kafkaui kafkaui 0 Jun 2 10:01 /tmp/pwned.poc178039439726378
docker exec cve-2024-32030-kafka-ui stat -c '%U %n' /tmp/pwned.poc178039439726378
-> kafkaui /tmp/pwned.poc178039439726378
Two properties together confirm that arbitrary code executed inside the vulnerable process:
- The nonce is fresh: the marker carries the nonce minted for this run only; no pre-planted or stale file could match it.
- The owner is
kafkaui: the file was created by the JVM'sRuntime.exec, which runs under the JVM's own identity (pid 1, userkafkaui). The exploit script never writes to the container's filesystem directly and shares no volume with it.
Corroborating (but not required): the listener log records call #1 -> delivering STAGE 1 (set property) then call #2 -> delivering STAGE 2 (CC7 exec). Kafka UI's own log shows a java.io.StreamCorruptedException from JmxMetricsRetriever.withJmxConnector at the moment the gadget fired; the exception is thrown after the gadget executes mid-deserialization in the vulnerable JMX frame. This is the exact code path that the fix removes.
Teardown¶
This stops and removes the Kafka UI container and clears any volumes, including the marker file.
4. Security Advice¶
Remediation¶
Upgrade to Kafka UI v0.7.2 or later. The fix was released on 2024-04-10 (commit 83b5a60, PR #4427). It removes commons-collections:3.2.2 from the transitive dependency tree and replaces it with commons-collections4:4.4, whose functor classes do not implement Serializable. With that replacement, neither the Scala bypass gadget nor the CC7 RCE gadget chain can be assembled from the classpath regardless of system property state.
If upgrading immediately is not possible:
- Disable dynamic cluster configuration. Remove or omit
-Ddynamic.config.enabled=true. Without this flag, new clusters cannot be registered through the unauthenticated HTTP API, blocking the primary attack surface. Clusters must be defined statically in the application's configuration file, where only administrators with file-system access can add them. - Enable authentication. Kafka UI supports several authentication mechanisms (OAuth2, LDAP, basic auth). Any of them prevents unauthenticated
PUT /api/configrequests, raising the bar from a network-adjacent attacker to one with valid credentials. - Restrict network access. If the Kafka UI instance is internal-only, firewall or service-mesh rules can limit which hosts can reach port 8080, reducing exposure even without application-layer controls.
Disabling dynamic config or adding authentication does not remove the vulnerable gadget classes from the classpath; it only removes the unauthenticated registration surface. A second attack path exists if an attacker has network-level access to a Kafka broker that the target Kafka UI is already configured to monitor, since they can then manipulate the JMX response directly. Upgrading to v0.7.2 is the only complete fix.
References¶
- NVD CVE-2024-32030 — CWE, CVSS, affected versions
- GitHub Security Lab Advisory GHSL-2023-230 — original reporter; JMX deserialization description
- Fix commit 83b5a60 — exact diff: exclude
commons-collections:3.x, addcommons-collections4:4.4 - Fix PR #4427 — "added commons-collections4 library instead of commons-collections", merged 2024-04-08
- v0.7.2 release notes — "CVEs fixes Apr 2024"
- Huawei PSIRT technical analysis (mirrored) — full Scala1 gadget payload code, step-by-step CC7 RCE reproduction
- Nuclei template PoC — detection-only template
- Apache Commons Collections security report — explains the CC 3.2.2
enableUnsafeSerializationguard