From c07d3b05255a21ecacf1bc246f2aa4a24107fbc6 Mon Sep 17 00:00:00 2001 From: Andreas Galauner Date: Tue, 28 Sep 2021 19:45:41 +0200 Subject: [PATCH 1/3] Add scripts to perform unicast and broadcast device discovery for Codesys V3 based PLCs --- nselib/codesys3.lua | 252 +++++++++++++++++++++++++ scripts/broadcast-codesys-discover.nse | 241 +++++++++++++++++++++++ scripts/codesys-plc-info.nse | 127 +++++++++++++ 3 files changed, 620 insertions(+) create mode 100644 nselib/codesys3.lua create mode 100644 scripts/broadcast-codesys-discover.nse create mode 100644 scripts/codesys-plc-info.nse diff --git a/nselib/codesys3.lua b/nselib/codesys3.lua new file mode 100644 index 000000000..108abc50f --- /dev/null +++ b/nselib/codesys3.lua @@ -0,0 +1,252 @@ +local math = require "math" +local unicode = require "unicode" +local ipOps = require "ipOps" +local stdnse = require "stdnse" +local string = require "string" +_ENV = stdnse.module("codesys3", stdnse.seeall); + +CodesysV3 = { + ServiceIDs = { + AddressServiceRequest = 0x01, + AddressServiceResponse = 0x02, + NameServiceRequest = 0x03, + NameServiceResponse = 0x04, + ChannelService = 0x40, + }, + + NameServicePackageTypes = { + ResolveName = 0xc201, + ResolveAddr = 0xc202, + ResolveGateway = 0xc203, + Identification = 0xc280, + }, + + -- The NameServiceRequest class contains functions to build a Codesys v3 Name Service Request as a broadcast + NameServiceRequest = { + + -- Creates a new NameServiceRequest instance + -- + -- @param portindex The codesys UDP port index + -- @param interface The interface this broadcast will be sent on - needed for codesys address calculation + -- @return o instance of request + new = function(self, portindex, address, netmask_cidr) + local o = { + --- data needed for dynamic calculations of fields + portindex = portindex, + address = address, + netmask_cidr = netmask_cidr, + + --- static header + magic = 0xc5, + hop_count = 0x0f, + header_length = 0x4, -- in 16 bit words + + priority = 0x01, + signal = 0, + address_type = 0, -- ABSOLUTE address for a broadcast + data_length = 0, + + service_id = CodesysV3.ServiceIDs.NameServiceRequest, + message_id = 0x00, + broadcast_id = math.random(0xffff), + + --- payload + package_type = CodesysV3.NameServicePackageTypes.ResolveAddr, + version = 0x0400, + request_id = math.random(math.pow(2, 32) - 1), + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Converts the whole request to a string + __tostring = function(self) + -- Build the address information - this is specific to the UDP broadcast packets in the Codesys V3 protocol + + -- N bits for the address in our subnet, 2 bits for the port index, rounded up to a multiple of 16 bits + local local_bits = 32 - self.netmask_cidr + local port_bits = 2 + local codesys_net_addr_words = ((local_bits + port_bits) + 15) // 16 + + -- We are broadcasting, so we don't have a receiver address, only our sender address + -- From here on all the lengths for the address fields are handled in 16 bit words + local sender_address_length = codesys_net_addr_words + local receiver_address_length = 0 + + -- Sanity check the address length fields - they need to be even (because they are in multiples of 16 bits) and need to fit in a 4 bit field + assert(sender_address_length <= 0xF, "Sender address length too big to fit in the field") + assert(receiver_address_length <= 0xF, "Receiver address length too big to fit in the field") + + local address_lengths = ((sender_address_length & 0xF) << 4) | (receiver_address_length & 0xF) + + -- mask off the local part of the address and add the port index in the front + local my_address = ipOps.todword(self.address) + local sender_address = ((self.portindex & 3) << local_bits) | (my_address & ((1 << local_bits) - 1)) + + -- Build a more or less static header for the packet including the length field for the sender and receiver address + local packet_header = string.pack(">BBBBBBH", + self.magic, + ((self.hop_count & 0x1F) << 3) | (self.header_length & 7), + ((self.priority & 3) << 6) | ((self.signal & 1) << 5) | ((self.address_type & 1) << 4) | (self.data_length & 0xF), + self.service_id, + self.message_id, + address_lengths, + self.broadcast_id + ) + + -- add the sender address to the packet + for i=0, sender_address_length-1 do + local j = sender_address_length - 1 - i; + packet_header = packet_header .. string.pack(">H", (sender_address >> (16 * j)) & 0xFFFF) + end + + -- pad the packet if necessary + if ( #packet_header % 4 ~= 0 ) then + packet_header = packet_header .. string.rep("\x00", #packet_header % 4) + end + + stdnse.debug1("Sending codesys v3 name service broadcast packet with header: %s", stdnse.tohex(packet_header)) + + -- append the name service request packet payload + local packet_payload = string.pack("BBBBBB", data) + + -- parse hopinfo field + o.hop_count = hopinfo >> 3 + o.header_length = hopinfo & 7 + + -- parse packetinfo field + o.priority = packetinfo >> 6 + o.signal = (packetinfo >> 5) & 1 + o.address_type = (packetinfo >> 4) & 1 + o.data_length = packetinfo & 0xf + + -- sanity check a few fields to determine if this is a name service broadcast response + if o.magic ~= 0xc5 or o.service_id ~= CodesysV3.ServiceIDs.NameServiceResponse then + return false, "Response has the wring magic value or isn't a Name Service Response" + end + + -- skip ahead to the address fields by skipping the length of the header + pos = o.header_length * 2 + + -- skip over the address fields using the address length fields in the header + pos = pos + (address_lengths & 0xf) * 2 + pos = pos + (address_lengths >> 4) * 2 + + -- skip the padding bytes if necessary + if pos % 4 ~= 0 then + pos = pos + (pos % 4) + end + + -- lua strings are 1-indexed, so adjust the position pointer... + pos = pos + 1 + + -- at this point we are at the packet payload + + -- parse the payload header + o.package_type, o.version, o.request_id, pos = string.unpack("> 24 & 0xff + local minor = v >> 16 & 0xff + local micro = v >> 8 & 0xff + local patch = v >> 0 & 0xff + return string.format("%d.%d.%d.%d", major, minor, micro, patch) +end + +return _ENV diff --git a/scripts/broadcast-codesys-discover.nse b/scripts/broadcast-codesys-discover.nse new file mode 100644 index 000000000..5f5a46047 --- /dev/null +++ b/scripts/broadcast-codesys-discover.nse @@ -0,0 +1,241 @@ +local nmap = require "nmap" +local ipOps = require "ipOps" +local packet = require "packet" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" +local codesys3 = require "codesys3" + + +description=[[ +Discovers hosts running a Codesys V3 PLC runtime on the LAN. It does so by +sending a broadcast packet with a device discovery request in a proprietary and +undocumented Codesys network protocol and then collects all responses from +devices on the network. +]] + +--- +-- @usage +-- nmap --script broadcast-codesys-discover +-- +-- @output +-- Pre-scan script results: +-- | broadcast-codesys-discover: +-- | 192.168.20.7: +-- | interface: enp11s0f0.20 +-- | deviceAddress: 192.168.20.7 +-- | targetVendor: 3S - Smart Software Solutions GmbH +-- | targetName: CODESYS Control for Raspberry Pi MC SL +-- | deviceName: raspberrypi +-- | targetID: 0x11 +-- | targetType: 0x1006 +-- | targetVersion: 3.5.15.10 +-- | 192.168.20.10: +-- | interface: enp11s0f0.20 +-- | deviceAddress: 192.168.20.10 +-- | targetVendor: WAGO +-- | targetName: WAGO 750-8215 PFC200 G2 4ETH CAN USB +-- | deviceName: PFC200V3-4538EF +-- | targetID: 0x1006120b +-- | targetType: 0x1000 +-- | targetVersion: 5.15.4.0 +-- | 192.168.20.9: +-- | interface: enp11s0f0.20 +-- | deviceAddress: 192.168.20.9 +-- | targetVendor: WAGO +-- | targetName: WAGO 750-8206 PFC200 2ETH RS CAN DPS +-- | deviceName: PFC200-438F4C +-- | targetID: 0x10061204 +-- | targetType: 0x1000 +-- |_ targetVersion: 5.15.4.0 +-- +-- @args broadcast-rip-discover.timeout timespec defining how long to wait for +-- a response. (default 3s) + +-- +-- Version 0.1 +-- Created 23/06/2021 - v0.1 - created by Andreas Galauner +-- + +author = "Andreas Galauner" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"broadcast", "safe"} + + +prerule = function() + if nmap.address_family() ~= 'inet' then + stdnse.debug1("is IPv4 compatible only.") + return false + end + + if not nmap.is_privileged() then + stdnse.verbose1("Not running due to lack of privileges.") + return false + end + + return true +end + +--- +-- Gets a list of available interfaces based on link and up filters +-- Interfaces are only added if they've got an ipv4 address +-- +-- @param link string containing the link type to filter +-- @param up string containing the interface status to filter +-- @return result table containing tables of interfaces +-- each interface table has the following fields: +-- name containing the device name +-- address containing the device address +-- netmask containing the device netmask +getInterfaces = function(link, up) + if( not(nmap.list_interfaces) ) then return end + local interfaces, err = nmap.list_interfaces() + local result = {} + if ( not(err) ) then + for _, iface in ipairs(interfaces) do + if ( iface.link == link and + iface.up == up and + iface.address ) then + + -- exclude ipv6 addresses for now + if ( not(iface.address:match(":")) ) then + table.insert(result, { name = iface.device, + address = iface.address, netmask = iface.netmask } ) + end + end + end + end + return result +end + +local function codeys_listener(sock, iface, timeout, result) + local condvar = nmap.condvar(result) + + local start_time = nmap.clock_ms() + local now = start_time + while( now - start_time < timeout ) do + sock:set_timeout(timeout - (now - start_time)) + local status, _, _, data = sock:pcap_receive() + + if ( status ) then + local p = packet.Packet:new( data, #data ) + if ( p and p.udp_dport ) then + local data = data:sub(p.udp_offset + 9) + local status, response = codesys3.CodesysV3.NameServiceResponse:new(data) + if ( status ) then + response.iface = iface.name + response.ip = p.ip_src + table.insert(result, response) + end + end + end + now = nmap.clock_ms() + end + sock:close() + condvar "signal" +end + +local function fail (err) return stdnse.format_output(false, err) end + +action = function() + local timeout = stdnse.parse_timespec(stdnse.get_script_args('broadcast-codesys-discover.timeout')) + timeout = (timeout or 3) * 1000 + + local iface = nmap.get_interface() + local interfaces = {} + + -- was an interface supplied using the -e argument? + if ( iface ) then + local iinfo, err = nmap.get_interface_info(iface) + + if ( not(iinfo.address) ) then + return fail("The IP address of the interface could not be determined") + end + + interfaces = { { name = iface, address = iinfo.address, netmask = iinfo.netmask } } + else + -- no interface was supplied, attempt autodiscovery + interfaces = getInterfaces("ethernet", "up") + end + + -- make sure we have at least one interface to run discovery on + if ( #interfaces == 0 ) then + return fail("Could not determine any valid interfaces, try to set one explicitly using -e") + end + + stdnse.debug1("Determined the following interfaces to run discovery on:") + for _, iface in ipairs(interfaces) do + stdnse.debug1("%s: IP: %s - Netmask: %s", iface.name, iface.address, iface.netmask) + stdnse.debug1("\tBroadcast address: %s", ipOps.get_last_ip(iface.address, iface.netmask)) + end + + local result = {} + local threads = {} + local condvar = nmap.condvar(result) + + -- start a listening thread for each interface + for _, iface in ipairs(interfaces) do + local sock, co + sock = nmap.new_socket() + sock:pcap_open(iface.name, 1500, false, "ip && udp && port 1743") + co = stdnse.new_thread(codeys_listener, sock, iface, timeout, result) + threads[co] = true + end + + -- Send out probes on all interfaces + for _, iface in ipairs(interfaces) do + local host = ipOps.get_last_ip(iface.address, iface.netmask) + local source_port = 1743 + + local socket = nmap.new_socket("udp") + socket:set_timeout(timeout) + + -- Send name service requests to all 4 codesys UDP ports + for i=0,3 do + local destination_port = 1740+i + local cs = codesys3.CodesysV3.NameServiceRequest:new(3, iface.address, iface.netmask) + local packet = tostring(cs) + + socket:bind(nil, source_port) + local status, err = socket:sendto(host, destination_port, packet) + + if ( not(status) ) then + return false, string.format("Failed to send broadcast packet to UDP port %d", destination_port) + end + end + end + + -- wait until all threads are done + 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 + + if not next(result) then + return nil + end + + -- Display the results + local response = stdnse.output_table() + + for _, r in ipairs(result) do + local out = stdnse.output_table() + + out["interface"] = r.iface + out["deviceAddress"] = r.ip + out["targetVendor"] = r.vendorName + out["targetName"] = r.deviceName + out["deviceName"] = r.nodeName + out["targetID"] = string.format("0x%x", r.targetId) + out["targetType"] = string.format("0x%x", r.targetType) + out["targetVersion"] = codesys3.version_to_str(r.targetVersion) + + response[r.ip] = out + end + + return response +end diff --git a/scripts/codesys-plc-info.nse b/scripts/codesys-plc-info.nse new file mode 100644 index 000000000..a4cc9ce9b --- /dev/null +++ b/scripts/codesys-plc-info.nse @@ -0,0 +1,127 @@ +local shortport = require "shortport" +local stdnse = require "stdnse" +local codesys3 = require "codesys3" + +description = [[ +Identifies a Codesys V3 PLC on the LAN by sending a Codesys V3 Device Discovery request. +]] + +--- +-- @usage +-- nmap --script codesys-plc-info +-- +-- @output +-- 1740/udp open|filtered encore +-- | codesys-plc-info: +-- | 192.168.20.12: +-- | deviceAddress: 192.168.20.12 +-- | targetVendor: WAGO +-- | targetName: WAGO 750-8215 PFC200 G2 4ETH CAN USB +-- | deviceName: PFC200V3-4538EF +-- | targetID: 0x1006120b +-- | targetType: 0x1000 +-- |_ targetVersion: 5.15.4.0 +-- +-- @args codesys-plc-info.timeout timespec defining how long to wait for a +-- response. (default 3s) + +-- +-- Version 0.1 +-- Created 23/06/2021 - v0.1 - created by Andreas Galauner +-- + +author = "Andreas Galauner" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"default", "discovery", "safe"} +portrule = shortport.portnumber({1740,1741,1742,1743}, "udp") + +--- 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 + +local function fail (err) return stdnse.format_output(false, err) end + +action = function(host, port) + local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) + timeout = (timeout or 3) * 1000 + + -- Try to determine the interface we can reach our target with. We need this for the name service request + local iface = getInterface(host) + if not iface then + return fail(string.format("Couldn't get interface for target IP address %s", host)) + end + + local socket = nmap.new_socket("udp") + socket:set_timeout(timeout) + + -- Bind to source port 1740, because we are using port index 0 in the name service request + -- We need to send the packet from this source port, otherwise the PLC doesn't seem to reply + local status, err = socket:bind(nil, 1740) + if not status then + return fail(string.format("Bind failed: %s", err)) + end + + -- Generate the name service request to send and send it out + local cs = codesys3.CodesysV3.NameServiceRequest:new(0, iface.address, iface.netmask) + local packet = tostring(cs) + + local status, err = socket:sendto(host, port, packet) + if not status then + return fail(string.format("Sendto failed: %s", err)) + end + + -- Receive the responses from the PLCs and parse them + local result = {} + repeat + local data + status, data = socket:receive() + if ( status ) then + local _, _, _, rhost, _ = socket:get_info() + local status, response = codesys3.CodesysV3.NameServiceResponse:new(data) + if ( status ) then + response.ip = rhost + table.insert(result, response) + end + end + until( not(status) ) + + socket:close() + + -- Display the results + local response = stdnse.output_table() + + for _, r in ipairs(result) do + local out = stdnse.output_table() + + out["deviceAddress"] = r.ip + out["targetVendor"] = r.vendorName + out["targetName"] = r.deviceName + out["deviceName"] = r.nodeName + out["targetID"] = string.format("0x%x", r.targetId) + out["targetType"] = string.format("0x%x", r.targetType) + out["targetVersion"] = codesys3.version_to_str(r.targetVersion) + + response[r.ip] = out + end + + return response +end From f681640d9de3d1c8a332488eb534b198a370009f Mon Sep 17 00:00:00 2001 From: Andreas Galauner Date: Tue, 5 Jul 2022 18:18:24 +0200 Subject: [PATCH 2/3] Incorporate the requested changes based on feedback in the GitHub pull request into the Codesys V3 discovery and info scripts --- nselib/codesys3.lua | 8 +-- scripts/broadcast-codesys-discover.nse | 41 +++++++++------ scripts/codesys-plc-info.nse | 69 +++++++++++++++++--------- 3 files changed, 73 insertions(+), 45 deletions(-) diff --git a/nselib/codesys3.lua b/nselib/codesys3.lua index 108abc50f..2c6223ca0 100644 --- a/nselib/codesys3.lua +++ b/nselib/codesys3.lua @@ -241,12 +241,6 @@ CodesysV3 = { } } -function version_to_str(v) - local major = v >> 24 & 0xff - local minor = v >> 16 & 0xff - local micro = v >> 8 & 0xff - local patch = v >> 0 & 0xff - return string.format("%d.%d.%d.%d", major, minor, micro, patch) -end +version_to_str = ipOps.fromdword return _ENV diff --git a/scripts/broadcast-codesys-discover.nse b/scripts/broadcast-codesys-discover.nse index 5f5a46047..2b22d63d0 100644 --- a/scripts/broadcast-codesys-discover.nse +++ b/scripts/broadcast-codesys-discover.nse @@ -87,8 +87,8 @@ end -- name containing the device name -- address containing the device address -- netmask containing the device netmask +-- broadcast containing the device broadcast address getInterfaces = function(link, up) - if( not(nmap.list_interfaces) ) then return end local interfaces, err = nmap.list_interfaces() local result = {} if ( not(err) ) then @@ -99,8 +99,12 @@ getInterfaces = function(link, up) -- exclude ipv6 addresses for now if ( not(iface.address:match(":")) ) then - table.insert(result, { name = iface.device, - address = iface.address, netmask = iface.netmask } ) + table.insert(result, { + name = iface.device, + address = iface.address, + netmask = iface.netmask, + broadcast = iface.broadcast + }) end end end @@ -108,7 +112,7 @@ getInterfaces = function(link, up) return result end -local function codeys_listener(sock, iface, timeout, result) +local function codesys_listener(sock, iface, timeout, result) local condvar = nmap.condvar(result) local start_time = nmap.clock_ms() @@ -152,7 +156,7 @@ action = function() return fail("The IP address of the interface could not be determined") end - interfaces = { { name = iface, address = iinfo.address, netmask = iinfo.netmask } } + interfaces = { { name = iface, address = iinfo.address, netmask = iinfo.netmask, broadcast = iinfo.broadcast } } else -- no interface was supplied, attempt autodiscovery interfaces = getInterfaces("ethernet", "up") @@ -165,8 +169,7 @@ action = function() stdnse.debug1("Determined the following interfaces to run discovery on:") for _, iface in ipairs(interfaces) do - stdnse.debug1("%s: IP: %s - Netmask: %s", iface.name, iface.address, iface.netmask) - stdnse.debug1("\tBroadcast address: %s", ipOps.get_last_ip(iface.address, iface.netmask)) + stdnse.debug1("%s: IP: %s - Netmask: %s - Broadcast: %s", iface.name, iface.address, iface.netmask, iface.broadcast) end local result = {} @@ -178,13 +181,12 @@ action = function() local sock, co sock = nmap.new_socket() sock:pcap_open(iface.name, 1500, false, "ip && udp && port 1743") - co = stdnse.new_thread(codeys_listener, sock, iface, timeout, result) - threads[co] = true + co, info = stdnse.new_thread(codesys_listener, sock, iface, timeout, result) + threads[co] = info end -- Send out probes on all interfaces for _, iface in ipairs(interfaces) do - local host = ipOps.get_last_ip(iface.address, iface.netmask) local source_port = 1743 local socket = nmap.new_socket("udp") @@ -196,8 +198,8 @@ action = function() local cs = codesys3.CodesysV3.NameServiceRequest:new(3, iface.address, iface.netmask) local packet = tostring(cs) - socket:bind(nil, source_port) - local status, err = socket:sendto(host, destination_port, packet) + socket:bind(iface.address, source_port) + local status, err = socket:sendto(iface.broadcast, destination_port, packet) if ( not(status) ) then return false, string.format("Failed to send broadcast packet to UDP port %d", destination_port) @@ -207,8 +209,8 @@ action = function() -- wait until all threads are done repeat - for thread in pairs(threads) do - if coroutine.status(thread) == "dead" then threads[thread] = nil end + for thread, info in pairs(threads) do + if info() == "dead" then threads[thread] = nil end end if ( next(threads) ) then condvar "wait" @@ -221,12 +223,12 @@ action = function() -- Display the results local response = stdnse.output_table() + local ips = stdnse.output_table() for _, r in ipairs(result) do local out = stdnse.output_table() out["interface"] = r.iface - out["deviceAddress"] = r.ip out["targetVendor"] = r.vendorName out["targetName"] = r.deviceName out["deviceName"] = r.nodeName @@ -235,6 +237,15 @@ action = function() out["targetVersion"] = codesys3.version_to_str(r.targetVersion) response[r.ip] = out + ips[#ips+1] = r.ip + end + + -- sort by IP address and reorder the results to get a stable output between different runs + ipOps.ip_sort(ips) + for _, ip in ipairs(ips) do + local tmp = response[ip] + response[ip] = nil + response[ip] = tmp end return response diff --git a/scripts/codesys-plc-info.nse b/scripts/codesys-plc-info.nse index a4cc9ce9b..c706ee350 100644 --- a/scripts/codesys-plc-info.nse +++ b/scripts/codesys-plc-info.nse @@ -1,5 +1,8 @@ local shortport = require "shortport" local stdnse = require "stdnse" +local nmap = require "nmap" +local string = require "string" +local table = require "table" local codesys3 = require "codesys3" description = [[ @@ -39,20 +42,39 @@ portrule = shortport.portnumber({1740,1741,1742,1743}, "udp") -- @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 + -- Check if we've been called by a host discovery scan + -- if this is the case, host.interface will be set and we will use this + if target.interface then + stdnse.debug1("Target interface has been passed to us from nmap - using %s", target.interface) + local interface, err = nmap.get_interface_info(target.interface) + + if err then + return fail(string.format("Couldn't get interface info for %s", target.interface)) + end + + stdnse.debug1("Using interface %s", interface.shortname) + return interface + end + + -- If not, create dummy UDP connection to get interface + stdnse.debug1("Target interface has NOT been passed to us from nmap - trying to detect the proper interface using the target") + 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() + + 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 + stdnse.debug1("Detected interface %s with address %s", interface.shortname, address) return interface end end @@ -75,18 +97,24 @@ action = function(host, port) -- Bind to source port 1740, because we are using port index 0 in the name service request -- We need to send the packet from this source port, otherwise the PLC doesn't seem to reply - local status, err = socket:bind(nil, 1740) + local status, err = socket:bind(iface.address, 1740) if not status then return fail(string.format("Bind failed: %s", err)) end + -- Connect the UDP socket to the target to be able to use send/recv + local status, err = socket:connect(host, port) + if not status then + return fail(string.format("Connect failed: %s", err)) + end + -- Generate the name service request to send and send it out local cs = codesys3.CodesysV3.NameServiceRequest:new(0, iface.address, iface.netmask) local packet = tostring(cs) - local status, err = socket:sendto(host, port, packet) + local status, err = socket:send(packet) if not status then - return fail(string.format("Sendto failed: %s", err)) + return fail(string.format("Send failed: %s", err)) end -- Receive the responses from the PLCs and parse them @@ -95,11 +123,12 @@ action = function(host, port) local data status, data = socket:receive() if ( status ) then - local _, _, _, rhost, _ = socket:get_info() local status, response = codesys3.CodesysV3.NameServiceResponse:new(data) if ( status ) then - response.ip = rhost - table.insert(result, response) + result = response + + -- One valid unicast response is enough for us, we can stop receiving more + break end end until( not(status) ) @@ -107,21 +136,15 @@ action = function(host, port) socket:close() -- Display the results - local response = stdnse.output_table() + local out = stdnse.output_table() - for _, r in ipairs(result) do - local out = stdnse.output_table() + out["deviceAddress"] = result.ip + out["targetVendor"] = result.vendorName + out["targetName"] = result.deviceName + out["deviceName"] = result.nodeName + out["targetID"] = string.format("0x%x", result.targetId) + out["targetType"] = string.format("0x%x", result.targetType) + out["targetVersion"] = codesys3.version_to_str(result.targetVersion) - out["deviceAddress"] = r.ip - out["targetVendor"] = r.vendorName - out["targetName"] = r.deviceName - out["deviceName"] = r.nodeName - out["targetID"] = string.format("0x%x", r.targetId) - out["targetType"] = string.format("0x%x", r.targetType) - out["targetVersion"] = codesys3.version_to_str(r.targetVersion) - - response[r.ip] = out - end - - return response + return out end From 2ea7104e2ca7b9018a4f863d5a83b7ba3fbb344b Mon Sep 17 00:00:00 2001 From: Andreas Galauner Date: Tue, 5 Jul 2022 19:25:36 +0200 Subject: [PATCH 3/3] Update the example output in the script to match the current output --- scripts/broadcast-codesys-discover.nse | 7 ++----- scripts/codesys-plc-info.nse | 14 ++++++-------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/scripts/broadcast-codesys-discover.nse b/scripts/broadcast-codesys-discover.nse index 2b22d63d0..c4f07884a 100644 --- a/scripts/broadcast-codesys-discover.nse +++ b/scripts/broadcast-codesys-discover.nse @@ -23,7 +23,6 @@ devices on the network. -- | broadcast-codesys-discover: -- | 192.168.20.7: -- | interface: enp11s0f0.20 --- | deviceAddress: 192.168.20.7 -- | targetVendor: 3S - Smart Software Solutions GmbH -- | targetName: CODESYS Control for Raspberry Pi MC SL -- | deviceName: raspberrypi @@ -32,7 +31,6 @@ devices on the network. -- | targetVersion: 3.5.15.10 -- | 192.168.20.10: -- | interface: enp11s0f0.20 --- | deviceAddress: 192.168.20.10 -- | targetVendor: WAGO -- | targetName: WAGO 750-8215 PFC200 G2 4ETH CAN USB -- | deviceName: PFC200V3-4538EF @@ -41,7 +39,6 @@ devices on the network. -- | targetVersion: 5.15.4.0 -- | 192.168.20.9: -- | interface: enp11s0f0.20 --- | deviceAddress: 192.168.20.9 -- | targetVendor: WAGO -- | targetName: WAGO 750-8206 PFC200 2ETH RS CAN DPS -- | deviceName: PFC200-438F4C @@ -49,8 +46,8 @@ devices on the network. -- | targetType: 0x1000 -- |_ targetVersion: 5.15.4.0 -- --- @args broadcast-rip-discover.timeout timespec defining how long to wait for --- a response. (default 3s) +-- @args broadcast-codesys-discover.timeout timespec defining how long to wait +-- for a response. (default 3s) -- -- Version 0.1 diff --git a/scripts/codesys-plc-info.nse b/scripts/codesys-plc-info.nse index c706ee350..36ea4347e 100644 --- a/scripts/codesys-plc-info.nse +++ b/scripts/codesys-plc-info.nse @@ -16,14 +16,12 @@ Identifies a Codesys V3 PLC on the LAN by sending a Codesys V3 Device Discovery -- @output -- 1740/udp open|filtered encore -- | codesys-plc-info: --- | 192.168.20.12: --- | deviceAddress: 192.168.20.12 --- | targetVendor: WAGO --- | targetName: WAGO 750-8215 PFC200 G2 4ETH CAN USB --- | deviceName: PFC200V3-4538EF --- | targetID: 0x1006120b --- | targetType: 0x1000 --- |_ targetVersion: 5.15.4.0 +-- | targetVendor: WAGO +-- | targetName: WAGO 750-8206 PFC200 2ETH RS CAN DPS +-- | deviceName: PFC200-438F4C +-- | targetID: 0x10061204 +-- | targetType: 0x1000 +-- |_ targetVersion: 5.15.4.0 -- -- @args codesys-plc-info.timeout timespec defining how long to wait for a -- response. (default 3s)