diff options
Diffstat (limited to 'scripts/knx-gateway-discover.nse')
-rw-r--r-- | scripts/knx-gateway-discover.nse | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/scripts/knx-gateway-discover.nse b/scripts/knx-gateway-discover.nse new file mode 100644 index 0000000..7446cbd --- /dev/null +++ b/scripts/knx-gateway-discover.nse @@ -0,0 +1,298 @@ +local nmap = require "nmap" +local coroutine = require "coroutine" +local stdnse = require "stdnse" +local table = require "table" +local packet = require "packet" +local ipOps = require "ipOps" +local string = require "string" +local target = require "target" +local knx = require "knx" + +description = [[ +Discovers KNX gateways by sending a KNX Search Request to the multicast address +224.0.23.12 including a UDP payload with destination port 3671. KNX gateways +will respond with a KNX Search Response including various information about the +gateway, such as KNX address and supported services. + +Further information: + * DIN EN 13321-2 + * http://www.knx.org/ +]] + +author = {"Niklaus Schiess <nschiess@ernw.de>", "Dominik Schneider <dschneider@ernw.de>"} +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"discovery", "safe", "broadcast"} + +--- +--@args timeout Max time to wait for a response. (default 3s) +-- +--@usage +-- nmap --script knx-gateway-discover -e eth0 +-- +--@output +-- Pre-scan script results: +-- | knx-gateway-discover: +-- | 192.168.178.11: +-- | Body: +-- | HPAI: +-- | Port: 3671 +-- | DIB_DEV_INFO: +-- | KNX address: 15.15.255 +-- | Decive serial: 00ef2650065c +-- | Multicast address: 0.0.0.0 +-- | Device MAC address: 00:05:26:50:06:5c +-- | Device friendly name: IP-Viewer +-- | DIB_SUPP_SVC_FAMILIES: +-- | KNXnet/IP Core version 1 +-- | KNXnet/IP Device Management version 1 +-- | KNXnet/IP Tunnelling version 1 +-- |_ KNXnet/IP Object Server version 1 +-- + +prerule = function() + if not nmap.is_privileged() then + stdnse.verbose1("Not running due to lack of privileges.") + return false + end + return true +end + +--- Sends a knx search request +-- @param query KNX search request message +-- @param mcat Multicast destination address +-- @param port Port to sent to +local knxSend = function(query, mcast, mport) + -- Multicast IP and UDP port + local sock = nmap.new_socket() + local status, err = sock:connect(mcast, mport, "udp") + if not status then + stdnse.debug1("%s", err) + return + end + sock:send(query) + sock:close() +end + +local fam_meta = { + __tostring = function (self) + return ("%s version %d"):format( + knx.knxServiceFamilies[self.service_id] or self.service_id, + self.Version + ) + end +} + +--- Parse a Search Response +-- @param knxMessage Payload of captures UDP packet +local knxParseSearchResponse = function(ips, results, knxMessage) + local knx_header_length, knx_protocol_version, knx_service_type, knx_total_length, pos = knx.parseHeader(knxMessage) + + if not knx_header_length then + stdnse.debug1("KNX header error: %s", knx_protocol_version) + return + end + + local message_format = '>B c1 c4 I2 BBB c1 I2 c2 c6 c4 c6 c30 BB' + if #knxMessage - pos + 1 < string.packlen(message_format) then + stdnse.debug1("Message too short for KNX message") + return + end + + local knx_hpai_structure_length, + knx_hpai_protocol_code, + knx_hpai_ip_address, + knx_hpai_port, + knx_dib_structure_length, + knx_dib_description_type, + knx_dib_knx_medium, + knx_dib_device_status, + knx_dib_knx_address, + knx_dib_project_install_ident, + knx_dib_dev_serial, + knx_dib_dev_multicast_addr, + knx_dib_dev_mac, + knx_dib_dev_friendly_name, + knx_supp_svc_families_structure_length, + knx_supp_svc_families_description, pos = string.unpack(message_format, knxMessage, pos) + + knx_hpai_ip_address = ipOps.str_to_ip(knx_hpai_ip_address) + + knx_dib_description_type = knx.knxDibDescriptionTypes[knx_dib_description_type] + knx_dib_knx_medium = knx.knxMediumTypes[knx_dib_knx_medium] + knx_dib_dev_multicast_addr = ipOps.str_to_ip(knx_dib_dev_multicast_addr) + knx_dib_dev_mac = stdnse.format_mac(knx_dib_dev_mac) + + local knx_supp_svc_families = {} + knx_supp_svc_families_description = knx.knxDibDescriptionTypes[knx_supp_svc_families_description] or knx_supp_svc_families_description + + for i=0,(knx_total_length - pos),2 do + local family = {} + family.service_id, family.Version, pos = string.unpack('BB', knxMessage, pos) + setmetatable(family, fam_meta) + knx_supp_svc_families[#knx_supp_svc_families+1] = family + end + + local search_response = stdnse.output_table() + if nmap.debugging() > 0 then + search_response.Header = stdnse.output_table() + search_response.Header["Header length"] = knx_header_length + search_response.Header["Protocol version"] = knx_protocol_version + search_response.Header["Service type"] = "SEARCH_RESPONSE (0x0202)" + search_response.Header["Total length"] = knx_total_length + + search_response.Body = stdnse.output_table() + search_response.Body.HPAI = stdnse.output_table() + search_response.Body.HPAI["Protocol code"] = stdnse.tohex(knx_hpai_protocol_code) + search_response.Body.HPAI["IP address"] = knx_hpai_ip_address + search_response.Body.HPAI["Port"] = knx_hpai_port + + search_response.Body.DIB_DEV_INFO = stdnse.output_table() + search_response.Body.DIB_DEV_INFO["Description type"] = knx_dib_description_type + search_response.Body.DIB_DEV_INFO["KNX medium"] = knx_dib_knx_medium + search_response.Body.DIB_DEV_INFO["Device status"] = stdnse.tohex(knx_dib_device_status) + search_response.Body.DIB_DEV_INFO["KNX address"] = knx.parseKnxAddress(knx_dib_knx_address) + search_response.Body.DIB_DEV_INFO["Project installation identifier"] = stdnse.tohex(knx_dib_project_install_ident) + search_response.Body.DIB_DEV_INFO["Decive serial"] = stdnse.tohex(knx_dib_dev_serial) + search_response.Body.DIB_DEV_INFO["Multicast address"] = knx_dib_dev_multicast_addr + search_response.Body.DIB_DEV_INFO["Device MAC address"] = knx_dib_dev_mac + search_response.Body.DIB_DEV_INFO["Device friendly name"] = knx_dib_dev_friendly_name + search_response.Body.DIB_SUPP_SVC_FAMILIES = knx_supp_svc_families + else + search_response.Body = stdnse.output_table() + search_response.Body.HPAI = stdnse.output_table() + search_response.Body.HPAI["Port"] = knx_hpai_port + + search_response.Body.DIB_DEV_INFO = stdnse.output_table() + search_response.Body.DIB_DEV_INFO["KNX address"] = knx.parseKnxAddress(knx_dib_knx_address) + search_response.Body.DIB_DEV_INFO["Decive serial"] = stdnse.tohex(knx_dib_dev_serial) + search_response.Body.DIB_DEV_INFO["Multicast address"] = knx_dib_dev_multicast_addr + search_response.Body.DIB_DEV_INFO["Device MAC address"] = knx_dib_dev_mac + search_response.Body.DIB_DEV_INFO["Device friendly name"] = knx_dib_dev_friendly_name + search_response.Body.DIB_SUPP_SVC_FAMILIES = knx_supp_svc_families + end + + ips[#ips+1] = knx_hpai_ip_address + results[knx_hpai_ip_address] = search_response +end + +--- Listens for knx search responses +-- @param interface Network interface to listen on. +-- @param timeout Maximum time to listen. +-- @param ips Table to put IP addresses into. +-- @param result Table to put responses into. +local knxListen = function(interface, timeout, ips, results) + local condvar = nmap.condvar(results) + local start = nmap.clock_ms() + local listener = nmap.new_socket() + local threads = {} + local status, l3data, _ + local filter = 'dst host ' .. interface.address .. ' and udp src port 3671' + listener:set_timeout(100) + listener:pcap_open(interface.device, 1024, true, filter) + + while (nmap.clock_ms() - start) < timeout do + status, _, _, l3data = listener:pcap_receive() + if status then + local p = packet.Packet:new(l3data, #l3data) + -- Skip IP and UDP headers + local knxMessage = string.sub(l3data, p.ip_hl*4 + 8 + 1) + local co = stdnse.new_thread(knxParseSearchResponse, ips, results, knxMessage) + threads[co] = true; + end + end + + repeat + for thread in pairs(threads) do + if coroutine.status(thread) == "dead" then threads[thread] = nil end + end + if ( next(threads) ) then + condvar "wait" + end + until next(threads) == nil; + condvar("signal") +end + +--- Returns the network interface used to send packets to a target host. +-- @param target host to which the interface is used. +-- @return interface Network interface used for target host. +local getInterface = function(target) + -- First, create dummy UDP connection to get interface + local sock = nmap.new_socket() + local status, err = sock:connect(target, "12345", "udp") + if not status then + stdnse.verbose1("%s", err) + return + end + local status, address, _, _, _ = sock:get_info() + if not status then + stdnse.verbose1("%s", err) + return + end + for _, interface in pairs(nmap.list_interfaces()) do + if interface.address == address then + return interface + end + end +end + +--- Make a dummy connection and return a free source port +-- @param target host to which the interface is used. +-- @return lport Local port which can be used in KNX messages. +local getSourcePort = function(target) + local socket = nmap.new_socket() + local _, _ = socket:connect(target, "12345", "udp") + local _, _, lport, _, _ = socket:get_info() + return lport +end + +action = function() + local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) + timeout = (timeout or 3) * 1000 + local ips, results = {}, {} + local mcast = "224.0.23.12" + local mport = 3671 + local lport = getSourcePort(mcast) + + -- Check if a valid interface was provided + local interface = nmap.get_interface() + if interface then + interface = nmap.get_interface_info(interface) + else + interface = getInterface(mcast) + end + if not interface then + return ("\n ERROR: Couldn't get interface for %s"):format(mcast) + end + + -- Launch listener thread + stdnse.new_thread(knxListen, interface, timeout, ips, results) + -- Craft raw query + local query = knx.query(0x0201, interface.address, lport) + -- Small sleep so the listener doesn't miss the response + stdnse.sleep(0.5) + -- Send query + knxSend(query, mcast, mport) + -- Wait for listener thread to finish + local condvar = nmap.condvar(results) + condvar("wait") + + -- Check responses + if #ips > 0 then + local sort_by_ip = function(a, b) + return ipOps.compare_ip(a, "lt", b) + end + table.sort(ips, sort_by_ip) + local output = stdnse.output_table() + + for i=1, #ips do + local ip = ips[i] + output[ip] = results[ip] + + if target.ALLOW_NEW_TARGETS then + target.add(ip) + end + end + + return output + end +end |