diff options
Diffstat (limited to 'scripts/mqtt-subscribe.nse')
-rw-r--r-- | scripts/mqtt-subscribe.nse | 393 |
1 files changed, 393 insertions, 0 deletions
diff --git a/scripts/mqtt-subscribe.nse b/scripts/mqtt-subscribe.nse new file mode 100644 index 0000000..43b48e5 --- /dev/null +++ b/scripts/mqtt-subscribe.nse @@ -0,0 +1,393 @@ +local mqtt = require "mqtt" +local nmap = require "nmap" +local shortport = require "shortport" +local stdnse = require "stdnse" +local table = require "table" + +description = [[ +Dumps message traffic from MQTT brokers. + +This script establishes a connection to an MQTT broker and subscribes +to the requested topics. The default topics have been chosen to +receive system information and all messages from other clients. This +allows Nmap, to listen to all messages being published by clients to +the MQTT broker. + +For additional information: +* https://en.wikipedia.org/wiki/MQTT +* https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html +]] + +--- +-- @usage nmap -p 1883 --script mqtt-subscribe <target> +-- +-- @output +-- PORT STATE SERVICE REASON +-- 1883/tcp open mosquitto version 1.4.8 syn-ack +-- | mqtt-subscribe: +-- | Topics and their most recent payloads: +-- | $SYS/broker/load/publish/received/5min: 0.27 +-- | $SYS/broker/publish/messages/received: 7 +-- | $SYS/broker/heap/current: 39240 +-- | $SYS/broker/load/messages/sent/15min: 21.54 +-- | $SYS/broker/load/bytes/sent/5min: 647.13 +-- | $SYS/broker/clients/disconnected: 40 +-- | $SYS/broker/clients/connected: 1 +-- | $SYS/broker/subscriptions/count: 40 +-- | $SYS/broker/load/publish/received/15min: 0.46 +-- | $SYS/broker/clients/inactive: 40 +-- | $SYS/broker/messages/sent: 2318 +-- | $SYS/broker/load/publish/sent/1min: 2.48 +-- | $SYS/broker/load/sockets/1min: 0.09 +-- | $SYS/broker/load/connections/15min: 0.41 +-- | $SYS/broker/load/bytes/sent/15min: 822.79 +-- | $SYS/broker/load/sockets/15min: 0.81 +-- | $SYS/broker/version: mosquitto version 1.4.8 +-- | $SYS/broker/load/messages/received/5min: 1.24 +-- | $SYS/broker/load/publish/sent/15min: 20.39 +-- | $SYS/broker/uptime: 225478 seconds +-- | $SYS/broker/load/publish/received/1min: 0.05 +-- | $SYS/broker/publish/messages/dropped: 0 +-- | $SYS/broker/retained messages/count: 47 +-- | $SYS/broker/messages/received: 293 +-- | $SYS/broker/load/connections/5min: 0.28 +-- | $SYS/broker/load/messages/sent/1min: 2.78 +-- | $SYS/broker/bytes/sent: 83026 +-- | $SYS/broker/load/bytes/received/5min: 13.98 +-- | $SYS/broker/load/messages/received/1min: 0.35 +-- | $SYS/broker/messages/stored: 47 +-- | $SYS/broker/publish/messages/sent: 2070 +-- | $SYS/broker/load/sockets/5min: 0.53 +-- | $SYS/broker/clients/active: 1 +-- | $SYS/broker/timestamp: Sun, 14 Feb 2016 15:48:26 +0000 +-- | $SYS/broker/load/bytes/received/15min: 17.83 +-- | $SYS/broker/publish/bytes/received: 49 +-- | $SYS/broker/load/publish/sent/5min: 16.03 +-- | $SYS/broker/publish/bytes/sent: 9752 +-- | $SYS/broker/load/bytes/sent/1min: 100.49 +-- | $SYS/broker/load/bytes/received/1min: 2.72 +-- | $SYS/broker/load/connections/1min: 0.06 +-- | $SYS/broker/clients/expired: 0 +-- | $SYS/broker/load/messages/received/15min: 1.49 +-- | $SYS/broker/load/messages/sent/5min: 17.00 +-- | $SYS/broker/bytes/received: 2520 +-- | $SYS/broker/heap/maximum: 41992 +-- |_ $SYS/broker/clients/total: 41 +-- +-- @xmloutput +-- <table key="Topics and their most recent payloads"> +-- <elem key="$SYS/broker/load/messages/sent/15min">23.48</elem> +-- <elem key="$SYS/broker/bytes/received">2469</elem> +-- <elem key="$SYS/broker/load/sockets/5min">0.63</elem> +-- <elem key="$SYS/broker/messages/sent">2268</elem> +-- <elem key="$SYS/broker/load/publish/sent/15min">22.25</elem> +-- <elem key="$SYS/broker/load/publish/received/1min">0.05</elem> +-- <elem key="$SYS/broker/load/bytes/sent/1min">626.45</elem> +-- <elem key="$SYS/broker/publish/messages/received">7</elem> +-- <elem key="$SYS/broker/load/connections/15min">0.39</elem> +-- <elem key="$SYS/broker/heap/current">38864</elem> +-- <elem key="$SYS/broker/load/sockets/1min">0.36</elem> +-- <elem key="$SYS/broker/messages/stored">47</elem> +-- <elem key="$SYS/broker/load/bytes/sent/15min">897.46</elem> +-- <elem key="$SYS/broker/version">mosquitto version 1.4.8</elem> +-- <elem key="$SYS/broker/clients/inactive">39</elem> +-- <elem key="$SYS/broker/subscriptions/count">39</elem> +-- <elem key="$SYS/broker/timestamp">Sun, 14 Feb 2016 15:48:26 +0000</elem> +-- <elem key="$SYS/broker/uptime">225280 seconds</elem> +-- <elem key="$SYS/broker/publish/bytes/sent">9520</elem> +-- <elem key="$SYS/broker/publish/messages/sent">2023</elem> +-- <elem key="$SYS/broker/load/bytes/received/1min">10.58</elem> +-- <elem key="$SYS/broker/load/connections/5min">0.31</elem> +-- <elem key="$SYS/broker/load/messages/received/15min">1.58</elem> +-- <elem key="$SYS/broker/publish/messages/dropped">0</elem> +-- <elem key="$SYS/broker/clients/connected">1</elem> +-- <elem key="$SYS/broker/load/messages/received/5min">1.51</elem> +-- <elem key="$SYS/broker/retained messages/count">47</elem> +-- <elem key="$SYS/broker/load/bytes/received/15min">18.78</elem> +-- <elem key="$SYS/broker/messages/received">289</elem> +-- <elem key="$SYS/broker/clients/disconnected">39</elem> +-- <elem key="$SYS/broker/load/publish/received/15min">0.46</elem> +-- <elem key="$SYS/broker/load/sockets/15min">0.82</elem> +-- <elem key="$SYS/broker/load/publish/sent/5min">21.44</elem> +-- <elem key="$SYS/broker/bytes/sent">81121</elem> +-- <elem key="$SYS/broker/publish/bytes/received">49</elem> +-- <elem key="$SYS/broker/load/connections/1min">0.18</elem> +-- <elem key="$SYS/broker/load/messages/received/1min">1.45</elem> +-- <elem key="$SYS/broker/clients/expired">0</elem> +-- <elem key="$SYS/broker/load/publish/received/5min">0.27</elem> +-- <elem key="$SYS/broker/load/messages/sent/5min">22.63</elem> +-- <elem key="$SYS/broker/load/bytes/received/5min">16.53</elem> +-- <elem key="$SYS/broker/load/messages/sent/1min">16.80</elem> +-- <elem key="$SYS/broker/clients/total">40</elem> +-- <elem key="$SYS/broker/clients/active">1</elem> +-- <elem key="$SYS/broker/load/publish/sent/1min">15.57</elem> +-- <elem key="$SYS/broker/load/bytes/sent/5min">863.85</elem> +-- <elem key="$SYS/broker/heap/maximum">41992</elem> +-- </table> +-- +-- @args mqtt-subscribe.client-id MQTT client identifier, defaults to +-- <code>nmap</code> with a random suffix. +-- @args mqtt-subscribe.listen-msgs Number of PUBLISH messages to +-- receive, defaults to 100. A value of zero forces this script +-- to stop only when listen-time has passed. +-- @args mqtt-subscribe.listen-time Length of time to listen for +-- PUBLISH messages, defaults to 5s. A value of zero forces this +-- script to stop only when listen-msgs PUBLISH messages have +-- been received. +-- @args mqtt-subscribe.password Password for MQTT brokers requiring +-- authentication. +-- @args mqtt-subscribe.protocol-level MQTT protocol level, defaults +-- to 4. +-- @args mqtt-subscribe.protocol-name MQTT protocol name, defaults to +-- <code>MQTT</code>. +-- @args mqtt-subscribe.topic Topic filters to indicate which PUBLISH +-- messages we'd like to receive. +-- @args mqtt-subscribe.username Username for MQTT brokers requiring +-- authentication. + +author = "Mak Kolybabi <mak@kolybabi.com>" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"safe", "discovery", "version"} + +portrule = shortport.version_port_or_service({1883, 8883}, {"mqtt", "secure-mqtt"}, "tcp") + +local function parse_args() + local args = {} + + local protocol_level = stdnse.get_script_args(SCRIPT_NAME .. '.protocol-level') + if protocol_level then + -- Sanity check the value from the user. + protocol_level = tonumber(protocol_level) + if type(protocol_level) ~= "number" then + return false, "protocol-level argument must be a number." + elseif protocol_level < 0 or protocol_level > 255 then + return false, "protocol-level argument must be in range between 0 and 255 inclusive." + end + else + -- Indicate to the library that it should choose this on its own. + protocol_level = false + end + args.protocol_level = protocol_level + + local protocol_name = stdnse.get_script_args(SCRIPT_NAME .. '.protocol-name') + if protocol_name then + -- Sanity check the value from the user. + if type(protocol_name) ~= "string" then + return false, "protocol-name argument must be a string." + end + else + -- Indicate to the library that it can choose this on its own. + protocol_name = false + end + args.protocol_name = protocol_name + + local client_id = stdnse.get_script_args(SCRIPT_NAME .. '.client-id') + if not client_id then + -- Indicate to the library that it should choose this on its own. + client_id = false + end + args.client_id = client_id + + local max_msgs = stdnse.get_script_args(SCRIPT_NAME .. '.listen-msgs') + if max_msgs then + -- Sanity check the value from the user. + max_msgs = tonumber(max_msgs) + if type(max_msgs) ~= "number" then + return false, "listen-msgs argument must be a number." + elseif max_msgs < 0 then + return false, "listen-msgs argument must be non-negative." + end + else + -- Many brokers have ~50 $SYS/# messages, so we double that number + -- for how many messages we'll receive. + max_msgs = 100 + end + args.max_msgs = max_msgs + + local max_time = stdnse.get_script_args(SCRIPT_NAME .. '.listen-time') + if max_time then + -- Convert the time specification from the CLI to seconds. + local err + max_time, err = stdnse.parse_timespec(max_time) + if not max_time then + return false, ("Unable to parse listen-time: %s"):format(err) + elseif max_time < 0 then + return false, "listen-time argument must be non-negative." + elseif args.max_msgs == 0 and max_time == 0 then + return false, "listen-time and listen-msgs may not both be zero." + end + else + max_time = 5 + end + args.max_time = max_time + + local username = stdnse.get_script_args(SCRIPT_NAME .. '.username') + if not username then + username = false + end + args.username = username + + local password = stdnse.get_script_args(SCRIPT_NAME .. '.password') + if password then + -- Sanity check the value from the user. + if not username then + return false, "A password cannot be given without also giving a username." + end + else + password = false + end + args.password = password + + local topic = stdnse.get_script_args(SCRIPT_NAME .. '.topic') + if topic then + -- Sanity check the value from the user. + if type(topic) ~= "table" then + topic = {topic} + end + else + -- These topic filters should receive most messages. + topic = {"$SYS/#", "#"} + end + args.topic = topic + + return true, args +end + +action = function(host, port) + local output = stdnse.output_table() + + -- Parse and sanity check the command line arguments. + local status, options = parse_args() + if not status then + output.ERROR = options + return output, output.ERROR + end + + -- Create an instance of the MQTT library's client object. + local helper = mqtt.Helper:new(host, port) + + -- Connect to the MQTT broker. + local status, response = helper:connect({ + ["protocol_level"] = options.protocol_level, + ["protocol_name"] = options.protocol_name, + ["client_id"] = options.client_id, + ["username"] = options.username, + ["password"] = options.password, + }) + if not status then + output.ERROR = response + return output, output.ERROR + elseif response.type ~= "CONNACK" then + output.ERROR = ("Received control packet type '%s' instead of 'CONNACK'."):format(response.type) + return output, output.ERROR + elseif not response.accepted then + output.ERROR = ("Connection rejected: %s"):format(response.reason) + return output, output.ERROR + end + + -- Build a list of topic filters. + local filters = {} + for _, filter in ipairs(options.topic) do + table.insert(filters, {["filter"] = filter}) + end + + -- Subscribe to receive PUBLISH messages that match our topic + -- filters.The MQTT standard allows sending PUBLISH messages before + -- the SUBACK message, so we explicitly ignore any non-CONNACK + -- messages at this point. + local status, response = helper:request("SUBSCRIBE", {["filters"] = filters}, "SUBACK") + if not status then + output.ERROR = response + return output, output.ERROR + end + + -- For each topic to which we tried to subscribe, the MQTT broker + -- informs us whether we were successful. We will note if any + -- subscriptions fail, but continue so long as any succeeded. + local success = false + local results = response.filters + for i, result in ipairs(results) do + local topic = options.topic[i] + if result.success then + stdnse.debug3("Topic filter '%s' was accepted with a maximum QoS of %d.", topic, result.max_qos) + success = true + else + stdnse.debug3("Topic filter '%s' was rejected.", topic) + end + end + + if not success then + output.ERROR = "Every topic filter was rejected." + return output, output.ERROR + end + + -- We are now in a position to receive PUBLISH messages for at least + -- one of our topic filters. We will record the topic of every + -- PUBLISH message, but only retain the most recent payload. + -- + -- We will continue to listen for PUBLISH messages until one of two + -- conditions is met, whichever comes first: + -- 1) We have listened for max_time + -- 2) We have received max_msgs + local end_time = nmap.clock_ms() + options.max_time * 1000 + local topics = {} + local keys = {} + local msgs = 0 + while true do + -- Check for the first condition. + local time_left = end_time - nmap.clock_ms() + if time_left <= 0 then + break + end + + status, response = helper:receive({"PUBLISH"}, time_left / 1000) + if not status then + break + end + + local name = response.topic + if not topics[name] then + table.insert(keys, name) + end + topics[name] = response.payload + + -- Check for the second condition. + msgs = msgs + 1 + if options.max_msgs ~= 0 and msgs >= options.max_msgs then + break + end + end + + -- Disconnect from the MQTT broker. + helper:close() + + -- We're not going to error out if the last response was an error if + -- there were successful responses before it, but we will log it. + if not status then + if #keys > 0 then + stdnse.debug3("Received error while listening for PUBLISH messages: %s", response) + else + output.ERROR = response + return output, output.ERROR + end + end + + -- Try and offer information on what software the MQTT broker is + -- running through the version identification interface. Sadly this + -- is often just a version number with no product name. + local ver = topics["$SYS/broker/version"] + if ver then + port.version.name = ver + nmap.set_port_version(host, port) + end + + -- Format the topics and payloads we received. + table.sort(keys) + local topics_in_order = {} + for _, key in ipairs(keys) do + topics_in_order[key] = topics[key] + end + + output["Topics and their most recent payloads"] = topics_in_order + return output, stdnse.format_output(true, output) +end |