1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 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
|