# This file is part of Scapy # See http://www.secdev.org/projects/scapy for more information # Copyright (C) Santiago Hernandez Ramos # This program is published under GPLv2 license # scapy.contrib.description = Message Queuing Telemetry Transport (MQTT) # scapy.contrib.status = loads from scapy.packet import Packet, bind_layers from scapy.fields import FieldLenField, BitEnumField, StrLenField, \ ShortField, ConditionalField, ByteEnumField, ByteField, PacketListField from scapy.layers.inet import TCP from scapy.error import Scapy_Exception from scapy.compat import orb, chb from scapy.volatile import RandNum from scapy.config import conf # CUSTOM FIELDS # source: http://stackoverflow.com/a/43717630 class VariableFieldLenField(FieldLenField): def addfield(self, pkt, s, val): val = self.i2m(pkt, val) data = [] while val: if val > 127: data.append(val & 127) val //= 128 else: data.append(val) lastoffset = len(data) - 1 data = b"".join(chb(val | (0 if i == lastoffset else 128)) for i, val in enumerate(data)) return s + data if len(data) > 3: raise Scapy_Exception("%s: malformed length field" % self.__class__.__name__) # If val is None / 0 return s + b"\x00" def getfield(self, pkt, s): value = 0 for offset, curbyte in enumerate(s): curbyte = orb(curbyte) value += (curbyte & 127) * (128 ** offset) if curbyte & 128 == 0: return s[offset + 1:], value if offset > 2: raise Scapy_Exception("%s: malformed length field" % self.__class__.__name__) def randval(self): return RandVariableFieldLen() class RandVariableFieldLen(RandNum): def __init__(self): RandNum.__init__(self, 0, 268435455) # LAYERS CONTROL_PACKET_TYPE = { 1: 'CONNECT', 2: 'CONNACK', 3: 'PUBLISH', 4: 'PUBACK', 5: 'PUBREC', 6: 'PUBREL', 7: 'PUBCOMP', 8: 'SUBSCRIBE', 9: 'SUBACK', 10: 'UNSUBSCRIBE', 11: 'UNSUBACK', 12: 'PINGREQ', 13: 'PINGRESP', 14: 'DISCONNECT', 15: 'AUTH' # Added in v5.0 } QOS_LEVEL = { 0: 'At most once delivery', 1: 'At least once delivery', 2: 'Exactly once delivery' } # source: http://stackoverflow.com/a/43722441 class MQTT(Packet): name = "MQTT fixed header" fields_desc = [ BitEnumField("type", 1, 4, CONTROL_PACKET_TYPE), BitEnumField("DUP", 0, 1, {0: 'Disabled', 1: 'Enabled'}), BitEnumField("QOS", 0, 2, QOS_LEVEL), BitEnumField("RETAIN", 0, 1, {0: 'Disabled', 1: 'Enabled'}), # Since the size of the len field depends on the next layer, we need # to "cheat" with the length_of parameter and use adjust parameter to # calculate the value. VariableFieldLenField("len", None, length_of="len", adjust=lambda pkt, x: len(pkt.payload),), ] PROTOCOL_LEVEL = { 3: 'v3.1', 4: 'v3.1.1', 5: 'v5.0' } class MQTTConnect(Packet): name = "MQTT connect" fields_desc = [ FieldLenField("length", None, length_of="protoname"), StrLenField("protoname", "", length_from=lambda pkt: pkt.length), ByteEnumField("protolevel", 5, PROTOCOL_LEVEL), BitEnumField("usernameflag", 0, 1, {0: 'Disabled', 1: 'Enabled'}), BitEnumField("passwordflag", 0, 1, {0: 'Disabled', 1: 'Enabled'}), BitEnumField("willretainflag", 0, 1, {0: 'Disabled', 1: 'Enabled'}), BitEnumField("willQOSflag", 0, 2, QOS_LEVEL), BitEnumField("willflag", 0, 1, {0: 'Disabled', 1: 'Enabled'}), BitEnumField("cleansess", 0, 1, {0: 'Disabled', 1: 'Enabled'}), BitEnumField("reserved", 0, 1, {0: 'Disabled', 1: 'Enabled'}), ShortField("klive", 0), FieldLenField("clientIdlen", None, length_of="clientId"), StrLenField("clientId", "", length_from=lambda pkt: pkt.clientIdlen), # Payload with optional fields depending on the flags ConditionalField(FieldLenField("wtoplen", None, length_of="willtopic"), lambda pkt: pkt.willflag == 1), ConditionalField(StrLenField("willtopic", "", length_from=lambda pkt: pkt.wtoplen), lambda pkt: pkt.willflag == 1), ConditionalField(FieldLenField("wmsglen", None, length_of="willmsg"), lambda pkt: pkt.willflag == 1), ConditionalField(StrLenField("willmsg", "", length_from=lambda pkt: pkt.wmsglen), lambda pkt: pkt.willflag == 1), ConditionalField(FieldLenField("userlen", None, length_of="username"), lambda pkt: pkt.usernameflag == 1), ConditionalField(StrLenField("username", "", length_from=lambda pkt: pkt.userlen), lambda pkt: pkt.usernameflag == 1), ConditionalField(FieldLenField("passlen", None, length_of="password"), lambda pkt: pkt.passwordflag == 1), ConditionalField(StrLenField("password", "", length_from=lambda pkt: pkt.passlen), lambda pkt: pkt.passwordflag == 1), ] RETURN_CODE = { 0: 'Connection Accepted', 1: 'Unacceptable protocol version', 2: 'Identifier rejected', 3: 'Server unavailable', 4: 'Bad username/password', 5: 'Not authorized' } class MQTTConnack(Packet): name = "MQTT connack" fields_desc = [ ByteField("sessPresentFlag", 0), ByteEnumField("retcode", 0, RETURN_CODE), # this package has not payload ] class MQTTPublish(Packet): name = "MQTT publish" fields_desc = [ FieldLenField("length", None, length_of="topic"), StrLenField("topic", "", length_from=lambda pkt: pkt.length), ConditionalField(ShortField("msgid", None), lambda pkt: (pkt.underlayer.QOS == 1 or pkt.underlayer.QOS == 2)), StrLenField("value", "", length_from=lambda pkt: (pkt.underlayer.len - pkt.length - 2)), ] class MQTTPuback(Packet): name = "MQTT puback" fields_desc = [ ShortField("msgid", None), ] class MQTTPubrec(Packet): name = "MQTT pubrec" fields_desc = [ ShortField("msgid", None), ] class MQTTPubrel(Packet): name = "MQTT pubrel" fields_desc = [ ShortField("msgid", None), ] class MQTTPubcomp(Packet): name = "MQTT pubcomp" fields_desc = [ ShortField("msgid", None), ] class MQTTSubscribe(Packet): name = "MQTT subscribe" fields_desc = [ ShortField("msgid", None), FieldLenField("length", None, length_of="topic"), StrLenField("topic", "", length_from=lambda pkt: pkt.length), ByteEnumField("QOS", 0, QOS_LEVEL), ] ALLOWED_RETURN_CODE = { 0: 'Success', 1: 'Success', 2: 'Success', 128: 'Failure' } class MQTTSuback(Packet): name = "MQTT suback" fields_desc = [ ShortField("msgid", None), ByteEnumField("retcode", None, ALLOWED_RETURN_CODE) ] class MQTTTopic(Packet): name = "MQTT topic" fields_desc = [ FieldLenField("len", None, length_of="topic"), StrLenField("topic", "", length_from=lambda pkt:pkt.len) ] def guess_payload_class(self, payload): return conf.padding_layer def cb_topic(pkt, lst, cur, remain): """ Decode the remaining bytes as a MQTT topic """ if len(remain) > 3: return MQTTTopic else: return conf.raw_layer class MQTTUnsubscribe(Packet): name = "MQTT unsubscribe" fields_desc = [ ShortField("msgid", None), PacketListField("topics", [], next_cls_cb=cb_topic) ] class MQTTUnsuback(Packet): name = "MQTT unsuback" fields_desc = [ ShortField("msgid", None) ] # LAYERS BINDINGS bind_layers(TCP, MQTT, sport=1883) bind_layers(TCP, MQTT, dport=1883) bind_layers(MQTT, MQTTConnect, type=1) bind_layers(MQTT, MQTTConnack, type=2) bind_layers(MQTT, MQTTPublish, type=3) bind_layers(MQTT, MQTTPuback, type=4) bind_layers(MQTT, MQTTPubrec, type=5) bind_layers(MQTT, MQTTPubrel, type=6) bind_layers(MQTT, MQTTPubcomp, type=7) bind_layers(MQTT, MQTTSubscribe, type=8) bind_layers(MQTT, MQTTSuback, type=9) bind_layers(MQTT, MQTTUnsubscribe, type=10) bind_layers(MQTT, MQTTUnsuback, type=11) bind_layers(MQTTConnect, MQTT) bind_layers(MQTTConnack, MQTT) bind_layers(MQTTPublish, MQTT) bind_layers(MQTTPuback, MQTT) bind_layers(MQTTPubrec, MQTT) bind_layers(MQTTPubrel, MQTT) bind_layers(MQTTPubcomp, MQTT) bind_layers(MQTTSubscribe, MQTT) bind_layers(MQTTSuback, MQTT) bind_layers(MQTTUnsubscribe, MQTT) bind_layers(MQTTUnsuback, MQTT)