Added support for MQTT protocol (#638)

Support (in contrib) for MQTT (Message Queue Telemetry Transport) protocol, with tests.
This commit is contained in:
shramos 2017-05-11 07:25:16 +02:00 committed by Pierre Lalet
parent 3123e90157
commit bab1b7d94d
2 changed files with 371 additions and 0 deletions

261
scapy/contrib/mqtt.py Normal file
View File

@ -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 <shramos@protonmail.com>
# 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)

110
scapy/contrib/mqtt.uts Normal file
View File

@ -0,0 +1,110 @@
# MQTT layer unit tests
# Copyright (C) Santiago Hernandez Ramos <shramos@protonmail.com>
#
# 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)