From bab1b7d94d2da8dedaaa842f5a82bc65f51c3241 Mon Sep 17 00:00:00 2001 From: shramos Date: Thu, 11 May 2017 07:25:16 +0200 Subject: [PATCH] Added support for MQTT protocol (#638) Support (in contrib) for MQTT (Message Queue Telemetry Transport) protocol, with tests. --- scapy/contrib/mqtt.py | 261 +++++++++++++++++++++++++++++++++++++++++ scapy/contrib/mqtt.uts | 110 +++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 scapy/contrib/mqtt.py create mode 100644 scapy/contrib/mqtt.uts diff --git a/scapy/contrib/mqtt.py b/scapy/contrib/mqtt.py new file mode 100644 index 000000000..5887a730e --- /dev/null +++ b/scapy/contrib/mqtt.py @@ -0,0 +1,261 @@ +# This file is part of Scapy +# See http://www.secdev.org/projects/scapy for more informations +# Copyright (C) Santiago Hernandez Ramos +# This program is published under GPLv2 license + + +from scapy.packet import Packet, bind_layers +from scapy.fields import FieldLenField, BitEnumField, StrLenField, \ + ShortField, ConditionalField, ByteEnumField, ByteField, StrNullField +from scapy.layers.inet import TCP +from scapy.error import Scapy_Exception + + +# 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 /= 127 + else: + data.append(val) + lastoffset = len(data) - 1 + data = "".join(chr(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__) + + def getfield(self, pkt, s): + value = 0 + for offset, curbyte in enumerate(s): + curbyte = ord(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__) + + +# 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'} + + +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),), + ] + + +class MQTTConnect(Packet): + name = "MQTT connect" + fields_desc = [ + FieldLenField("length", None, length_of="protoname"), + StrLenField("protoname", "", + length_from=lambda pkt: pkt.length), + ByteField("protolevel", 0), + 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 MQTTUnsubscribe(Packet): + name = "MQTT unsubscribe" + fields_desc = [ + ShortField("msgid", None), + StrNullField("payload", "") + ] + + +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) diff --git a/scapy/contrib/mqtt.uts b/scapy/contrib/mqtt.uts new file mode 100644 index 000000000..17d92c2d3 --- /dev/null +++ b/scapy/contrib/mqtt.uts @@ -0,0 +1,110 @@ +# MQTT layer unit tests +# Copyright (C) Santiago Hernandez Ramos +# +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('mqtt')" -t scapy/contrib/mqtt.uts + ++ Syntax check += Import the MQTT layer +from scapy.contrib.mqtt import * + + ++ MQTT protocol test + += MQTTPublish, packet instanciation +p = MQTT()/MQTTPublish(topic='test1',value='test2') +assert(p.type == 3) +assert(p.topic == 'test1') +assert(p.value == 'test2') +assert(p.len == None) +assert(p.length == None) + += Fixed header and MQTTPublish, packet dissection +s = b'0\n\x00\x04testtest' +publish = MQTT(s) +assert(publish.type == 3) +assert(publish.QOS == 0) +assert(publish.DUP == 0) +assert(publish.RETAIN == 0) +assert(publish.len == 10) +assert(publish[MQTTPublish].length == 4) +assert(publish[MQTTPublish].topic == 'test') +assert(publish[MQTTPublish].value == 'test') + + += MQTTConnect, packet instanciation +c = MQTT()/MQTTConnect(clientIdlen=5, clientId='newid') +assert(c.type == 1) +assert(c.clientId == 'newid') +assert(c.clientIdlen == 5) + += MQTTConnect, packet dissection +s = b'\x10\x1f\x00\x06MQIsdp\x03\x02\x00<\x00\x11mosqpub/1440-kali' +connect = MQTT(s) +assert(connect.length == 6) +assert(connect.protoname == 'MQIsdp') +assert(connect.protolevel == 3) +assert(connect.usernameflag == 0) +assert(connect.passwordflag == 0) +assert(connect.willretainflag == 0) +assert(connect.willQOSflag == 0) +assert(connect.willflag == 0) +assert(connect.cleansess == 1) +assert(connect.reserved == 0) +assert(connect.klive == 60) +assert(connect.clientIdlen == 17) +assert(connect.clientId == 'mosqpub/1440-kali') + + +=MQTTConnack, packet instanciation +ck = MQTT()/MQTTConnack(sessPresentFlag=1,retcode=0) +assert(ck.type == 2) +assert(ck.sessPresentFlag == 1) +assert(ck.retcode == 0) + += MQTTConnack, packet dissection +s = b' \x02\x00\x00' +connack = MQTT(s) +assert(connack.sessPresentFlag == 0) +assert(connack.retcode == 0) + + += MQTTSubscribe, packet instanciation +sb = MQTT()/MQTTSubscribe(msgid=1,topic='newtopic',QOS=0,length=0) +assert(sb.type == 8) +assert(sb.msgid == 1) +assert(sb.topic == 'newtopic') +assert(sb.length == 0) +assert(sb[MQTTSubscribe].QOS == 0) + += MQTTSubscribe, packet dissection +s = b'\x82\t\x00\x01\x00\x04test\x00' +subscribe = MQTT(s) +assert(subscribe.msgid == 1) +assert(subscribe.length == 4) +assert(subscribe.topic == 'test') +assert(subscribe.QOS == 1) + + += MQTTSuback, packet instanciation +sk = MQTT()/MQTTSuback(msgid=1, retcode=0) +assert(sk.type == 9) +assert(sk.msgid == 1) +assert(sk.retcode == 0) + += MQTTSuback, packet dissection +s = b'\x90\x03\x00\x01\x00' +suback = MQTT(s) +assert(suback.msgid == 1) +assert(suback.retcode == 0) + + += MQTTPubrec, packet instanciation +pc = MQTT()/MQTTPubrec(msgid=1) +assert(pc.type == 5) +assert(pc.msgid == 1) + += MQTTPubrec packet dissection +s = b'P\x02\x00\x01' +pubrec = MQTT(s) +assert(pubrec.msgid == 1)