Repository: secdev/scapy

Issue:
MQTT UNSUBSCRIBE payload
#### Brief description

MQTTUnsubscribe packet treats unsubscribe payload as **StrNullField**. According to the specification, the [unsubscribe payload](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718075) should be a (list of) **StrLenField**.

#### Environment

- Python 3.7
- Scapy 2.4.2

#### How to reproduce
Let's take the following to packets to go through the scenario shown in [MQTT spec. Table 3.7](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718075)

```
# unsubscribe with a single topic a/b
u1 = MQTT(b'\xA2\x07\x00\x01\x00\x03\x61\x2F\x62')

# unsubscribe with two topics a/b and c/d
u2 = MQTT(b'\xA2\x0C\x00\x01\x00\x03\x61\x2F\x62\x00\x03\x63\x2F\x64')
```
#### Actual result

```
>>> u1
<MQTT  type=UNSUBSCRIBE DUP=Disabled QOS=At least once delivery RETAIN=Disabled len=7 |<MQTTUnsubscribe  msgid=1 payload='' |<MQTT  type=0 DUP=Disabled QOS=At least once delivery RETAIN=Enabled len=97 |<Raw  load='/b' |>>>>
```
What happend here? Following assumption for u1 with a single topic:

> b'**\xA2\x07\x00\x01**\x00\x03\x61\x2F\x62'

Decodes correctly to a unsubscribe packet with **len=7** and **msgid=1**
Now, the subscribe payload should be decoded. This one contains a length field (`\x00\x03`) and the topic (`\x61\x2F\x62`)

> b'\x00\x03\x61\x2F\x62'

However, at this point, the payload is decoded as [StrNullField](https://github.com/secdev/scapy/blob/master/scapy/contrib/mqtt.py#L237) which leads to an empty payload and an early finish of the packet (we parsed so far 3 bytes, while remaining length field stated we will have 7 bytes). So the last 4 bytes are leftover and treated as a new layer, which is definitely wrong. 

#### Expected result

A first naive solution for this issue would be to treat the payload as length encoded string as practised in other packets like [publish](https://github.com/secdev/scapy/blob/master/scapy/contrib/mqtt.py#L168-L170) and [subscribe](https://github.com/secdev/scapy/blob/master/scapy/contrib/mqtt.py#L212-L214).

```
class MQTTUnsubscribe(Packet):
    name = "MQTT unsubscribe"
    fields_desc = [
        ShortField("msgid", None),
        FieldLenField("length", None, length_of="payload"),
        StrLenField("payload", "",
                    length_from=lambda pkt: pkt.length)
    ]
```

With this fix, Unsubscribe with a single topic will work as intended:

```
>>> u1 = MQTT(b'\xA2\x07\x00\x01\x00\x03\x61\x2F\x62')
>>> u1
<MQTT  type=UNSUBSCRIBE DUP=Disabled QOS=At least once delivery RETAIN=Disabled len=7 |<MQTTUnsubscribe  msgid=1 length=3 payload='a/b' |>>
```

While this works for unsubscribe packets with a single topic within the payload, it still fails on a list of topics (the same issue applies also to the [MQTT Subscribe payload](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718066) implemented in the [MQTTSubscribe Packet](https://github.com/secdev/scapy/blob/master/scapy/contrib/mqtt.py#L212-L215)).

```
>>> u2 = MQTT(b'\xA2\x0C\x00\x01\x00\x03\x61\x2F\x62\x00\x03\x63\x2F\x64')
>>> u2
<MQTT  type=UNSUBSCRIBE DUP=Disabled QOS=At least once delivery RETAIN=Disabled len=12 |<MQTTUnsubscribe  msgid=1 length=3 payload='a/b' |<MQTT  type=0 DUP=Disabled QOS=At most once delivery RETAIN=Disabled len=3 |<Raw  load='c/d' |>>>>
```

I assume the first scenario (_u1_) is more common, so the naive fix should satisfy most cases. The second scenario (_u2_) could be still reproduced by repeated independent unsubscribe message.
However, the MQTT feature subscribing/unsubscribing to/from multiple topics with one single message would be still missing.  Unfortunately, I couldn't find a way to describe such a structure with Scapy's fields properly.

#### Related resources

[MQTT Unsubscribe payload](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718075)
[MQTT Subscribe payload](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718066)



Discussion:
Hi & thanks for the report !

In the example you provided (u2), what was it supposed to look like ?

Would something like below work ? (I haven't read the RFC. This is pure guessing from what you've given so far :smile:)
`bind_layers(MQTTUnsubscribe, MQTTUnsubscribe)`
Hi,

> bind_layers(MQTTUnsubscribe, MQTTUnsubscribe)

I don't think this would work. As far as I understand, this would mean having one fixed header and two [variable headers](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718065) and two payloads. This would lead to additional issues and break the [structure](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718019). As stated in [[MQTT-3.10.3-2]](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718075) the payload contains _at least_ one topic filter, which means it can also have more than just two of them.

> In the example you provided (u2), what was it supposed to look like ?

Actually, I don't know how this would look like in Scapy as I couldn't find a similar structure so far. 
By following the above example I would expect u2 to be something like this:

```
>>> u2.show()
###[ MQTT fixed header ]###
  type      = UNSUBSCRIBE
  DUP       = Disabled
  QOS       = At least once delivery
  RETAIN    = Disabled
  len       = 12
###[ MQTT unsubscribe ]###
     msgid     = 1
     payload = [
     {length    = 3,  topic   = 'a/b'},
     {length    = 3,  topic   = 'c/d'}
]
```

Resolving patch:
diff --git a/scapy/contrib/mqtt.py b/scapy/contrib/mqtt.py
--- a/scapy/contrib/mqtt.py
+++ b/scapy/contrib/mqtt.py
@@ -8,11 +8,12 @@
 
 from scapy.packet import Packet, bind_layers
 from scapy.fields import FieldLenField, BitEnumField, StrLenField, \
-    ShortField, ConditionalField, ByteEnumField, ByteField, StrNullField
+    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
@@ -230,11 +231,32 @@ class MQTTSuback(Packet):
     ]
 
 
+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),
-        StrNullField("payload", "")
+        PacketListField("topics", [], next_cls_cb=cb_topic)
     ]
 
 


Test patch:
diff --git a/test/contrib/mqtt.uts b/test/contrib/mqtt.uts
--- a/test/contrib/mqtt.uts
+++ b/test/contrib/mqtt.uts
@@ -126,3 +126,6 @@ assert(bytes(p) == b'\x10\x00')
 assert(type(MQTT().fieldtype['len'].randval()) == RandVariableFieldLen)
 assert(type(MQTT().fieldtype['len'].randval() + 0) == int)
 
+= MQTTUnsubscribe
+u = MQTT(b'\xA2\x0C\x00\x01\x00\x03\x61\x2F\x62\x00\x03\x63\x2F\x64')
+assert MQTTUnsubscribe in u and len(u.topics) == 2 and u.topics[1].topic == b"c/d"