diff --git a/scripts/multicast-profinet-discovery.nse b/scripts/multicast-profinet-discovery.nse index 060265fad..8bb8684d5 100755 --- a/scripts/multicast-profinet-discovery.nse +++ b/scripts/multicast-profinet-discovery.nse @@ -1,12 +1,13 @@ local coroutine = require "coroutine" +local math = require "math" local nmap = require "nmap" local stdnse = require "stdnse" local string = require "string" local table = require "table" +local target = require "target" local packet = require "packet" local ipOps = require "ipOps" - description = [[ Sends a multicast PROFINET DCP Identify All message and prints the responses. @@ -16,27 +17,36 @@ Reference: ---@output --multicast-profinet-discovery: ---| devices: +--| 00:0E:8C:C9:41:15: +--| Interface: eth0 +--| IP: +--| ip_info: IP set +--| ip_addr: 10.253.81.37 +--| subnetmask: 255.255.255.0 +--| gateway: 10.253.81.1 +--| Device: +--| vendorId: 002A +--| deviceId: 0105 +--| vendorValue: S7-300 +--| deviceRole: 0x00 (None) +--| nameOfStation: pn-io +--| instance: low: 0, high: 100 --| ---| ip_addr: 10.253.81.37 ---| mac_addr: 00:0E:8C:C9:41:15 ---| subnetmask: 255.255.255.0 ---| vendorId: 002A ---| deviceId: 0105 ---| vendorvalue: S7-300 ---| deviceRole: 00 ---| nameOfStation: pn-io ---| ---| ip_addr: 10.253.81.26 ---| mac_addr: AC:64:17:2C:C9:46 ---| subnetmask: 255.255.255.0 ---| vendorId: 002A ---| deviceId: 0404 ---| vendorvalue: SIMATIC-HMI ---| deviceRole: 00 ---|_ nameOfStation: xd134xbvisu.profinetxaschnittstellexb103b2 +--| AC:64:17:2C:C9:46: +--| Interface: eth0 +--| IP: +--| ip_info: IP set +--| ip_addr: 10.253.81.26 +--| subnetmask: 255.255.255.0 +--| gateway: 10.253.81.1 +--| Device: +--| vendorId: 002A +--| deviceId: 0404 +--| vendorValue: SIMATIC-HMI +--| deviceRole: 0x01 (IO-Device) +--|_ nameOfStation: xd134xbvisu.profinetxaschnittstellexb103b2 -author = "Stefan Eiwanger, DINA-community" +author = {"Stefan Eiwanger, DINA-community", "Andreas Galauner"} license = "BSD-2-Clause Plus Patent License. For further details, please refer https://spdx.org/licenses/BSD-2-Clause-Patent.html" categories = {"discovery","info", "safe", "broadcast"} @@ -59,274 +69,212 @@ build_eth_frame= function(iface) stdnse.debug(1, "Build packet for dcp identify all call.") stdnse.debug(1, "Interface: " .. iface.device) - local pn_dcp_size = 46 -- min size of ethernet packet - local eth_packet - local src_mac = iface.mac + local eth_packet = packet.Frame:new() + eth_packet.mac_src = iface.mac - local dest_mac = packet.mactobin(pn_dcp_multicast) - local eth_proto = string.pack("I2", 0x9288) + eth_packet.mac_dst = packet.mactobin(pn_dcp_multicast) + eth_packet.ether_type = 0x8892 -- pn-dcp request frame : [FrameID | ServiceID | ServiceType | Xid | ResponseDelay | DCPDataLength | Option | Suboption ] - local blockData = string.pack("I2BBI4I2I2BB", 0xfefe, 0x05,0x00,0x10000010, 0x0400, 0x0400,0xff, 0xff) - local padbyte = string.pack("B", 0x00) + eth_packet.buf = string.pack(">I2BBI4I2I2BBI2", + 0xfefe, -- Frame ID + 0x05, -- Service ID: 5 = Identify + 0x00, -- Service Type: 0 = Request + math.random(0xffffffff), -- Xid (transaction ID) + math.random(9), -- Response delay * 10ms + 0x0004, -- DCP Data length (length of following data) + 0xff, -- Option: 0xff = all + 0xff, -- Suboption: 0xff = all + 0x0000 -- Length of following block data: 0 + ) -- build the packet - eth_packet = dest_mac .. src_mac .. eth_proto .. blockData - local length = string.len(eth_packet) + eth_packet:build_ether_frame() -- fill the rest of the packet with 0x00 till ethernet min size is reached - local padding = string.rep(padbyte, (pn_dcp_size-length)) - eth_packet = eth_packet .. padding - return eth_packet + return eth_packet.frame_buf end +local PNDCP_IP_INFO = { + [0] = "No IP set", + [1] = "IP set", + [2] = "IP set via DHCP", +} + +local PNDCP_DEVICE_ROLES = { + [0x01] = "IO-Device", + [0x02] = "IO-Controller", + [0x04] = "IO-Multidevice", + [0x08] = "PN-Supervisor", +} + +local function parse_string (block) + -- skip 2-byte block info + return block:sub(3) +end + +local function create_parser (parsefunc, label) + return function (block, results) + results[label] = parsefunc(block) + end +end + +local parser = { + -- Option IP + ['\x01\x01'] = function (block, results) + local _, mac = string.unpack(">I2 c6") + results.mac_addr = stdnse.format_mac(mac) + end, + ['\x01\x02'] = function (block, results) + local block_info, ipdw, netdw, gwdw = string.unpack(">I2 I4 I4 I4", block) + + local ipinfo = PNDCP_IP_INFO[block_info & 0xf] + if block_info & 0x80 > 0 then + ipinfo = ipinfo .. " (conflict)" + end + results.ip_info = ipinfo + + if ipdw > 0 then + results.ip_addr = ipOps.fromdword(ipdw) + end + if netdw > 0 then + results.subnetmask = ipOps.fromdword(netdw) + end + if gwdw > 0 then + results.gateway = ipOps.fromdword(gwdw) + end + end, + -- device properties + ['\x02\x01'] = function (block, results) + results.vendorValue = block:sub(3) + end, + ['\x02\x02'] = function (block, results) + results.nameOfStation = block:sub(3) + end, + ['\x02\x03'] = function (block, results) + local vendorid, deviceid = string.unpack(">xx I2 I2", block) + results.vendorId = ("0x%04x"):format(vendorid) + results.deviceId = ("0x%04x"):format(deviceid) + end, + ['\x02\x04'] = function (block, results) + local deviceRole = string.unpack(">xxBx", block) + + -- device role + local device_role_strings = {} + if deviceRole == 0x00 then + table.insert(device_role_strings, "None") + else + for flag, name in pairs(PNDCP_DEVICE_ROLES) do + if deviceRole & flag ~= 0 then + table.insert(device_role_strings, name) + end + end + end + results.deviceRole = ("0x%02x (%s)"):format(deviceRole, + table.concat(device_role_strings, ", ")) + end, + --['\x02\x05'] device options? + ['\x02\x06'] = function (block, results) + results.alias = block:sub(3) + end, + ['\x02\x07'] = function (block, results) + local low, high = string.unpack(">xx BB", block) + results.instance = ("low: %d, high: %d"):format(low, high) + end, + ['\x02\x08'] = function (block, results) + local vendorid, deviceid = string.unpack(">xx I2 I2", block) + results.OEMvendorId = ("0x%04x"):format(vendorid) + results.OEMdeviceId = ("0x%04x"):format(deviceid) + end, +} + +-- ensure any option can be used without crashing +setmetatable(parser, { + __index = function(self, key) + local option, suboption = string.byte(key, 1, 2) + stdnse.debug(1, "Unknown option/suboption %d/%d", option, suboption) + return function () end + end, + }) -- extract data from incoming dcp packets and store them into a table ---@param eth_data ethernet part of the recieved packet --@param pn_data profinet part of the recieved packet == ethernet packetload --@return device table with all extraced data from the pn_dcp -parse_pndcp = function(eth_data, pn_data) +parse_pndcp = function(pn_data) stdnse.debug(1, "Start parsing of answer") - local pos = 7 -- start after the destination mac address (host) - local deviceMacAddress - local deviceRoleInterpretation = {} - deviceRoleInterpretation [0] = "PNIO Device" - deviceRoleInterpretation [1] = "PNIO Controller" - deviceRoleInterpretation [2] = "PNIO Multidevice" - deviceRoleInterpretation [3] = "PNIO Supervisor" - - -- extract device mac address - local mac - mac, pos = string.unpack("c6", eth_data, pos) - deviceMacAddress = stdnse.format_mac(mac) - - stdnse.debug(1, "Device MAC address: %s", deviceMacAddress) -- check if the packet is a request - local serviceType - serviceType= string.unpack("B", pn_data, 4) - stdnse.debug(1, "Servicetype %x", serviceType) - if (serviceType == 0) then return end - - - -- start extrating data from pn_dcp_response -- start with 1 - pos = 11 - - local gesDCPDataLength = "" - gesDCPDataLength, pos = string.unpack(">I2", pn_data, pos) - stdnse.debug(1,"DCP Datalength of full packet: %d", gesDCPDataLength) + local dcp_header_format = ">I2 B B xxxx xx xx" -- skip Xid, delay, length + if #pn_data < dcp_header_format:packsize() then + return nil + end + local frame_id, service_id, service_type, pos = string.unpack(dcp_header_format, pn_data) + if frame_id ~= 0xfeff or service_id ~= 5 or service_type ~= 1 then + return nil + end -- extract data from DCP block - local option, suboption - local IP, deviceVendorValue, deviceRole, deviceId, nameofstation, dcpDatalength, subnetmask, standardGateway, vendorId = "", "", "", "", "", "", "", "", "" - stdnse.debug(1, "Start extracting data from DCP block") - while(pos < gesDCPDataLength) do + local result = {} + while(pos < #pn_data) do - -- Option IP, suboption IP - option, suboption, pos = string.unpack("BB", pn_data, pos) - - local dcpDataLength, _ - if option == 1 then -- IP - if(suboption == 2) then - stdnse.debug(1, "Option IP, suboption IP") - - -- DCP block length - dcpDataLength, pos = string.unpack(">I2", pn_data, pos) - --stdnse.debug(1,"* DCP Datalength of IP/IP %d", dcpDataLength) - - -- block info - _, pos = string.unpack(">I2", pn_data, pos) - - local dword = "" - -- IP - dword, pos = string.unpack(">I4", pn_data, pos) - IP = ipOps.fromdword(dword) - stdnse.debug(1, "* IP address: %s", IP) - - -- subnetmask - dword, pos = string.unpack(">I4", pn_data, pos) - subnetmask = ipOps.fromdword(dword) - stdnse.debug(1, "* Subnetmask: %s", subnetmask) - - -- standard gateway - dword, pos = string.unpack(">I4", pn_data, pos) - standardGateway = ipOps.fromdword(dword) - stdnse.debug(1, "* Default gateway: %s", standardGateway) - - --[[if dcpDataLength%2 ~= 0 then - pos = pos +1 -- add padding - end - --]] - else - stdnse.debug(1, "Option IP, suboption something else: %d", suboption) - - -- DCP block length - dcpDataLength, pos = string.unpack(">I2", pn_data, pos) - --stdnse.debug(1, "* DCP datalength of IP/else: %d", dcpDataLength) - - if dcpDataLength%2 ~= 0 then - pos = pos +1 -- add padding - stdnse.debug(1, "dcpDatalength was odd, add padding +1 to pos") - end - - end - elseif option == 2 then -- device properties - if suboption == 1 then-- deviceVendorValue manufacturer specific option - stdnse.debug(1, "Option device properties, suboption manufacturer specific") - - -- DCP block length - dcpDataLength, pos = string.unpack(">I2", pn_data, pos) - --stdnse.debug(1,"* DCP Datalength of device properties/manufacturer specific %d", dcpDataLength) - - -- block info - _, pos = string.unpack(">I2", pn_data, pos) - - -- device vendor - deviceVendorValue, pos = string.unpack("c" .. (dcpDataLength - 2) ,pn_data, pos) - stdnse.debug(1, "* Device Vendor: %s", deviceVendorValue) - - if dcpDataLength%2 ~= 0 then - stdnse.debug(1, "dcpDatalength was odd, add padding +1 to pos") - pos = pos +1 -- add padding - end - - elseif suboption == 2 then -- nameofstation - stdnse.debug(1, "Option device properties, suboption name of station") - - -- DCP block length - dcpDataLength, pos = string.unpack(">I2", pn_data, pos) - --stdnse.debug(1,"* DCP Datalength of device properties/name of station %d", dcpDataLength) - - -- block info - _, pos = string.unpack(">I2", pn_data, pos) - - -- name of station - nameofstation, pos = string.unpack("c" .. (dcpDataLength - 2) ,pn_data, pos) - stdnse.debug(1, "* Name Of Station: %s", nameofstation) - - if dcpDataLength%2 ~= 0 then - stdnse.debug(1, "dcpDatalength was odd, add padding +1 to pos") - pos = pos +1 -- add padding - end - - elseif suboption == 3 then -- device id, vendor Id - stdnse.debug(1, "Option device properties, suboption device ID") - - -- DCP block length - dcpDataLength, pos = string.unpack(">I2", pn_data, pos) - --stdnse.debug(1,"* DCP Datalength of device properties/device ID %d", dcpDataLength) - - -- block info - _, pos = string.unpack(">I2", pn_data, pos) - - -- vendor ID - local tmpvendorId, tmpdeviceId = "", "" - tmpvendorId, pos = string.unpack("c2", pn_data, pos) - vendorId = stdnse.tohex(tmpvendorId) - vendorId = "0x" .. vendorId - stdnse.debug(1, "* Vendor ID: %s", vendorId) - - -- device ID - tmpdeviceId, pos = string.unpack("c2", pn_data, pos) - deviceId = stdnse.tohex(tmpdeviceId) - deviceId = "0x" .. deviceId - stdnse.debug(1, "* Device ID: %s", deviceId) - - elseif suboption == 4 then -- device role - stdnse.debug(1, "Option device properties, suboption device role") - - -- DCP block length - dcpDataLength, pos = string.unpack(">I2", pn_data, pos) - --stdnse.debug(1,"* DCP Datalength of device properties/device role %d", dcpDataLength) - - -- block info - _, pos = string.unpack(">I2", pn_data, pos) - - -- device role - deviceRole, pos = string.unpack("B", pn_data, pos) - deviceRole = deviceRoleInterpretation[deviceRole] .. ' 0x0' .. deviceRole - stdnse.debug(1, "* Device Role: %s", deviceRole) - - -- reserved - _, pos = string.unpack("B", pn_data, pos) - else - stdnse.debug(1, "Option device properties, suboption something else: %d", suboption) - - -- DCP block length - dcpDataLength, pos = string.unpack(">I2", pn_data, pos) - --stdnse.debug(1,"* DCP Datalength of device properties/device role %d", dcpDataLength) - - pos = pos + dcpDataLength - if dcpDataLength%2 ~= 0 then - stdnse.debug(2, "dcpDatalength was odd, add padding +1 to pos") - pos = pos +1 -- add padding - end - - end - else - stdnse.debug(1, "Option something else: %d", option) - - -- DCP block length - dcpDataLength, pos = string.unpack(">I2", pn_data, pos) - --stdnse.debug(1,"* DCP Datalength of device properties/device role %d", dcpDataLength) - - pos = pos + dcpDataLength - if dcpDataLength%2 ~= 0 then - stdnse.debug(1, "dcpDatalength was odd, add padding +1 to pos") - pos = pos +1 -- add padding - end - - end -- close if + local option, block, p = string.unpack("!2 c2 >s2", pn_data, pos) + parser[option](block, result) end -- close while - -- store data into table - local device = stdnse.output_table() - device.ip_addr = IP - device.mac_addr = deviceMacAddress - device.subnetmask = subnetmask - device.vendorId = vendorId - device.deviceId = deviceId - device.vendorvalue = deviceVendorValue - device.deviceRole = deviceRole - device.nameOfStation = nameofstation - - stdnse.debug(1, "End of parsing\n") - - return device + return result end -- helpfunction for thread call --@param iface interface table +--@param to_ms timeout in ms to wait for responses --@param pn_dcp ethernet dcp packet to send --@param devices table for results --@return devices, table with devices which answered to the dcp identify all call -discoverThread = function(iface, pn_dcp, devices) +discoverThread = function(iface, to_ms, pn_dcp, devices) local condvar = nmap.condvar(devices) local dnet = nmap.new_dnet() local pcap_s = nmap.new_socket() - pcap_s:set_timeout(2000) + pcap_s:set_timeout(100) dnet:ethernet_open(iface.device) pcap_s:pcap_open(iface.device, 256, false, "ether proto 0x8892") - local status, ethData, length, pn_data - dnet:ethernet_send(pn_dcp) -- send the frame + dnet:ethernet_close(); -- close the sender - status = true - while status do - status, length, ethData, pn_data = pcap_s:pcap_receive() + local start = nmap.clock_ms() + while (nmap.clock_ms() - start) < to_ms do + local status, length, ethData, pn_data = pcap_s:pcap_receive() if(status) then - devices[#devices + 1] = parse_pndcp(ethData, pn_data) + local dev = parse_pndcp(pn_data) + if dev then + local out = stdnse.output_table() + out.Interface = iface.device + out.IP = stdnse.output_table() + if dev.ip_addr then + -- Add new target if desired + target.add(dev.ip_addr) + out.IP.ip_addr = dev.ip_addr + end + out.IP.ip_info = dev.ip_info + out.IP.subnetmask = dev.subnetmask + out.IP.gateway = dev.gateway + out.Device = stdnse.output_table() + out.Device.vendorId = dev.vendorId + out.Device.deviceId = dev.deviceId + out.Device.vendorValue = dev.vendorValue + out.Device.deviceRole = dev.deviceRole + out.Device.nameOfStation = dev.nameOfStation + -- extract device mac address + local mac = string.unpack("c6", ethData, 7) + devices[stdnse.format_mac(mac)] = out + end end end - dnet:ethernet_close(iface.device); -- close the sender - - pcap_s:close(iface.device) condvar "signal" @@ -334,19 +282,13 @@ discoverThread = function(iface, pn_dcp, devices) end -- main fuction ---@return 0 if no devices were found --@return output_tab table for nmap to show the gathered information action = function() local output_tab = stdnse.output_table() - output_tab.devices = {} -- check interface parameter - local dnet = nmap.new_dnet() - local pcap_s = nmap.new_socket() - pcap_s:set_timeout(4000) - local macs = {} local filter_interfaces = function (iface) if iface.link == "ethernet" and iface.up == "up" and @@ -360,22 +302,22 @@ action = function() -- check if at least one interface is available if #interfaces == 0 then print("No interfaces found") - return false + return end - -- get the frame we want to send - + local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) + local to_ms = (timeout or 2) * 1000 local threads = {} - local condvar = nmap.condvar(output_tab.devices) + local condvar = nmap.condvar(output_tab) for _, iface in ipairs(interfaces) do local pn_dcp = build_eth_frame(iface) --print(iface.device) - local co = stdnse.new_thread(discoverThread, iface, pn_dcp, output_tab.devices) + local co = stdnse.new_thread(discoverThread, iface, to_ms, pn_dcp, output_tab) threads[co] = true end @@ -392,9 +334,9 @@ action = function() until next(threads) == nil -- check the output if something is doubled there - if #output_tab.devices == 0 then + if #output_tab == 0 then print("No profinet devices in the subnet") - return 0 + return end