Blog

Pentesting Kafka, RabbitMQ and MQTT with AI agents: the broker as the shortest path to your data

Autonomous security testing for Kafka, RabbitMQ and MQTT: management APIs, anonymous access, weak topic authorization and secrets, with proof of impact.

· 8 min read

Message brokers are where the important data actually moves. Kafka carries the event stream that the whole platform reacts to, RabbitMQ routes the jobs that pay the bills, and MQTT connects the fleet of devices that never had a security review in the first place. Yet brokers sit inside the trust boundary by convention rather than by control. They are deployed once, wired into everything, and then left alone, on the assumption that nothing outside the network can reach them. That assumption is exactly what an attacker tests, and it is exactly what almost nobody tests on purpose.

Darkmoon ships a specialist agent for this surface. Its messaging-cache agent understands Redis, RabbitMQ, Kafka, NATS, MQTT, ActiveMQ and ZooKeeper as first class targets rather than generic open ports. It knows each broker's management API, its default credentials, its authorization model and the RCE adjacent primitives that live inside it. This article walks through how one agent reasons across that whole family, and where it stops itself from over claiming.

Brokers are trusted infrastructure that rarely gets tested

A broker is a strange thing to secure because its job is to accept and hand out messages quickly, to as many producers and consumers as possible. Authentication and authorization are optional features on most of them, off by default on several, and disabled deliberately by teams who found them inconvenient during a migration and never turned them back on. The result is a service that trusts the network, sitting on a network that trusts the service. When one host inside that perimeter is compromised, or when a management port is quietly bound to a routable interface, the broker becomes the single richest pivot in the environment.

This is the same failure class we have documented on adjacent infrastructure: an unauthenticated Redis instance in our Redis exploitation write up, and a leaked Docker socket in the container escape case study. Brokers belong in that group. The difference is that people expect a database to need a password, and they do not always expect the same of a queue.

One agent, many protocols

Darkmoon reasons, plans and dispatches 50 specialist agents that run real offensive operations through a controlled MCP layer, backed by 50+ security tools in the image. The messaging-cache agent is the one that owns the broker family, and the value of a single agent across seven protocols is that it carries the cross protocol knowledge a human would: it recognises that a RabbitMQ management API and a Kafka Connect REST endpoint are both HTTP control planes worth probing, that MQTT and Kafka both fail open on topic access, and that ZooKeeper is often the soft underbelly behind a hardened Kafka cluster.

The agent works the way an operator does, with curl and jq against REST management APIs, native protocol clients for the wire level checks, and a strict rule that default credentials are tried from a capped, well known list rather than brute forced. It never runs an unbounded command. The table below is the mental map it applies.

BrokerControl plane the agent probesPrimitive it looks for
RabbitMQManagement HTTP API (:15672)Default guest/guest, queue read and write, config export
KafkaBroker protocol, Kafka Connect RESTAnonymous topic access, connector abuse
MQTTBroker (:1883 / :8883)Anonymous connect, wildcard subscribe, retained secrets
NATSMonitoring (:8222), client (:4222)No auth token, subject wildcard access
ActiveMQWeb console, OpenWireDefault admin/admin, unsafe deserialization
ZooKeeperClient port (:2181)Anonymous znode read, config and ACL disclosure
RedisClient port (:6379)No auth, keyspace read, MODULE LOAD

Authorization first

Everything below is offensive technique. Run it only against brokers you own or are explicitly contracted to test, and only within an agreed scope and change window. Broker testing touches live message flows, so the rules of engagement matter as much as the technique. Darkmoon runs non-destructively by default and cleans up after itself, but that is not a substitute for written authorization.

Anonymous and default credential exposure of management interfaces

The first thing the agent checks is whether it needs credentials at all, and if it does, whether the shipped defaults still work. This is not exotic. RabbitMQ ships with a guest/guest account, ActiveMQ with admin/admin, and both management consoles are routinely reachable on interfaces they were never meant to bind to. Against RabbitMQ the agent talks to the management HTTP API directly, which turns a working login into a full inventory in a few requests.

curl -s -u guest:guest http://<broker>:15672/api/overview | jq '{rabbitmq_version, node}'
curl -s -u guest:guest http://<broker>:15672/api/vhosts | jq -r '.[].name'
curl -s -u guest:guest http://<broker>:15672/api/queues \
  | jq -r '.[] | "\(.vhost)\t\(.name)\t\(.messages) msgs"'

The same pattern applies to a NATS monitoring port that answers on /varz without a token, or an MQTT broker that accepts an anonymous CONNECT. In each case the agent records the exact request and the raw response, because a management endpoint answering 200 is a lead, not yet a finding. The agent's job is to prove the endpoint hands over something it should not, then decide how much that something is worth.

Weak authorization: reading and writing across every queue, topic and key namespace

Authentication is only half the problem. The more common and more damaging failure is authorization that does not partition anything, so any authenticated principal, or any anonymous one, can read and write across every queue, topic, subject or key namespace on the broker. On Kafka this shows up as a client that can subscribe to any topic and, worse, produce to any topic. On MQTT it is a subscriber that can register for the # wildcard and receive every message on the broker, including the retained messages that devices leave behind. On RabbitMQ it is one account with access to every vhost.

The impact is direct. Read access across topics means the agent can harvest whatever is flowing: credentials, tokens, personal data, internal identifiers. Write access is worse, because a broker is a control plane for the systems that consume it. Publishing a crafted message onto a topic that a downstream service trusts is command injection by another name, mediated by the queue. The agent demonstrates read impact by pulling a bounded sample of live messages and recording what class of data appeared, and it treats write access as a separate, higher severity finding that it confirms by the broker's own acknowledgement rather than by disrupting a real consumer.

# MQTT: does the broker let an anonymous client subscribe to everything?
mosquitto_sub -h <broker> -t '#' -C 20 -W 10 -v
# -> topic/path  {"deviceId":"HOST_INTERNAL_001","token":"<captured, in the report>"}

# Kafka: can we read a topic we were never granted?
kafka-console-consumer --bootstrap-server <broker>:9092 \
  --topic payments.events --from-beginning --max-messages 5

This is where Darkmoon's privacy design matters in practice. When the agent harvests a message that contains a real host, IP or credential, the reasoning model never sees the real value. The gateway replaces it with a deterministic placeholder such as HOST_INTERNAL_001 before anything reaches the LLM, and rehydrates the real value locally only at the moment a tool runs. That is why the sample above shows a placeholder where a device identifier would be. We are precise about the limits of this: deterministic placeholders leak structure and cardinality by design, and the command gateway is a policy layer rather than a sandbox. The full treatment is in how we run an AI pentest without sending data to the LLM.

Secrets in messages and configs, and RCE adjacent primitives

Brokers accumulate secrets in two places: inside the messages themselves, and inside their own configuration. The agent reads both. Configuration export through a management API frequently discloses connection strings, federation credentials and the shovel or connector definitions that hold credentials for other systems in plain text. A single RabbitMQ definitions export can hand over the keys to a database, an object store and a second broker at once.

Beyond disclosure, several brokers carry primitives that sit one step away from code execution, and the agent knows each of them by name. On Redis, MODULE LOAD can load a shared object and run native code, though we have published a case where that exact primitive was correctly demoted to mitigated because Redis 7.x refused it, and the agent reported the smaller true result rather than the bigger false one. On Kafka, a writable Kafka Connect REST endpoint lets an attacker create a connector, which is arbitrary class loading and configuration on the worker. On ActiveMQ, unsafe deserialization in older OpenWire handling has a long history of turning a message into remote code execution.

The agent does not fire these blindly. It qualifies each one against the specific version and configuration in front of it, because a primitive that exists in the protocol is not the same as a primitive that works on this deployment. That discipline is the same one we apply to broader cloud work in the autonomous cloud penetration testing write up.

Proving impact at cluster scale, then cleaning up test artifacts

A broker is rarely a single node, so the agent has to reason about the cluster. Reading the topology through ZooKeeper or the broker's own metadata tells it which nodes exist, which topics are replicated where, and which management endpoints are reachable. Proving impact then means showing that an issue holds across the cluster rather than on one lucky node, and doing it without leaving the environment in a worse state than it started.

Every claim the agent makes is graded by the same qualification rule that runs inside all of its agents. A finding is marked EXPLOITED only when the impact was executed end to end, so data was actually extracted or an action was actually performed. It is marked CONFIRMED when the agent demonstrated impact with an exact request or payload, the raw response, and the extracted data or execution trace, without necessarily carrying the action through to a destructive end. It is marked UNCONFIRMED, capped at low severity, when there is a real lead but the impact was not demonstrated. The agent adversarially challenges its own strong claims, so a bare 200 response, an echoed payload, or a secret that was public by design gets demoted rather than counted.

Cleanup is part of the run, not an afterthought. If the agent creates a test queue, publishes a probe message, or provisions a temporary connector to prove write access, it removes that artifact and records the removal in the same finding, the way it minted and then deleted an AWS access key in our Terraform state campaign. A test that proves impact and then quietly leaves a live connector behind is not a test we would ship.

What we do not claim

Reachability and default configuration decide most of this. The techniques here work against brokers that are exposed without authentication, run with shipped defaults, or authorize every principal across every namespace. They are not a claim that a broker with mutual TLS, per topic ACLs and a hardened management plane falls the same way. The version and configuration in front of the agent decide which RCE adjacent primitive is real, which is exactly why the agent qualifies each one instead of asserting it.

Remediation

  • Turn authentication on and remove every shipped default. Delete or rename the RabbitMQ guest account, change ActiveMQ's admin/admin, and require a token on NATS. Never rely on the network being private.
  • Bind management interfaces to loopback or a management network only. A RabbitMQ management console, a Kafka Connect REST port or a NATS monitoring endpoint should never be reachable from where producers and consumers live.
  • Enforce least privilege authorization per queue, topic, subject and vhost. Deny topic and key namespace wildcards, and separate producer rights from consumer rights so read access never implies write access.
  • Keep secrets out of messages and out of broker configuration. Reference a secret manager for shovel, federation and connector credentials instead of embedding them in a definitions export.
  • Disable the dangerous primitives you do not use. Restrict Redis commands including MODULE LOAD, lock down or remove Kafka Connect where it is not needed, and keep ActiveMQ patched against the deserialization classes.
  • Encrypt broker traffic with TLS and, where the protocol supports it, mutual TLS, so that harvesting the message stream requires more than reaching the port.

FAQ

Do message brokers really need penetration testing? Yes, and more than most services, because a broker sits inside the trust boundary and touches every system that produces or consumes from it. Authentication and authorization are optional on most brokers and disabled on many, so a broker is often the shortest path from one compromised host to the whole platform's data.

How does the agent find an unauthenticated broker? It probes the control plane each broker exposes, the RabbitMQ management API, the Kafka and Kafka Connect endpoints, the MQTT and NATS ports, the ZooKeeper client port, and checks whether it can act without credentials or with the shipped defaults. A management endpoint that answers is treated as a lead, then the agent tries to prove it hands over data or accepts writes before recording a finding.

Can weak broker authorization lead to code execution? It can, through primitives that live in specific brokers: a writable Kafka Connect REST endpoint, ActiveMQ deserialization, or Redis MODULE LOAD. The agent qualifies each against the exact version and configuration in front of it, because the primitive existing in the protocol is not the same as it working on this deployment. Even without RCE, write access to a topic a downstream service trusts is a serious impact on its own.

Is testing MQTT different from testing Kafka? The reasoning is shared but the surface differs. MQTT testing centres on anonymous connects, wildcard subscriptions and retained secrets left by devices, which overlaps with the device work in our IoT firmware testing write up. Kafka testing centres on topic authorization, the broker protocol and the Connect control plane. One agent carries both mental models, which is the point of a broker specialist rather than a generic port scanner.

Will the test disrupt live queues or topics? Darkmoon runs non-destructively by default. It reads bounded samples to prove impact, confirms write access through the broker's own acknowledgement rather than by flooding a real consumer, and removes any test artifact it creates, recording the removal in the finding. Broker testing still belongs in an agreed change window, which is why authorization and rules of engagement come first.

What this proves about autonomous pentesting

The interesting thing about the broker family is not any single exploit. It is that seven protocols with different wire formats share the same underlying failures: trusting the network, shipping usable defaults, and authorizing everyone across everything. A single agent that carries the cross protocol knowledge can walk that whole surface the way an operator would, prove each issue to the standard its qualification rule demands, and clean up behind itself, all while the reasoning model only ever sees deterministic placeholders in place of your real hosts and credentials. That is the case for testing brokers continuously rather than assuming the perimeter will keep them private.

Darkmoon is GPL-3.0 and self hosted: source, docs.

Run it against your own lab

Darkmoon is open source (GPL-3.0) and self hosted. Clone it, point it at a target you own, and read every line.