mirror of
https://github.com/nmap/nmap.git
synced 2026-08-04 14:49:29 +00:00
Merge 2ea7104e2c into 8508d02033
This commit is contained in:
commit
bbb32a5d94
3 changed files with 643 additions and 0 deletions
246
nselib/codesys3.lua
Normal file
246
nselib/codesys3.lua
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
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("<HHI",
|
||||
0xc202, -- Packet type - 0xC202 = Resolve Address
|
||||
0x0400, -- Version - 0x0400 = Version 4.0
|
||||
0x04206969 -- "Randome" Request ID
|
||||
)
|
||||
|
||||
stdnse.debug1("Payload of name service broadcast: %s", stdnse.tohex(packet_payload))
|
||||
|
||||
return packet_header .. packet_payload
|
||||
end,
|
||||
|
||||
},
|
||||
|
||||
-- The NameServiceResponse class contains functions to parse a Codesys v3 Name Service Response
|
||||
NameServiceResponse = {
|
||||
-- Creates a new Response instance based on raw socket data
|
||||
--
|
||||
-- @param data string containing the raw socket response
|
||||
-- @return o Response instance
|
||||
new = function(self, data)
|
||||
local o = { data = data }
|
||||
|
||||
if ( not(data) or #data < 6 ) then
|
||||
return false, "Response isn't long enough the be a Name Service Response"
|
||||
end
|
||||
|
||||
local hopinfo, packetinfo, address_lengths, pos
|
||||
o.magic, hopinfo, packetinfo, o.service_id, o.message_id, address_lengths, pos = string.unpack(">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("<HHI", data, pos)
|
||||
|
||||
|
||||
-- check the package type we are expecting
|
||||
if o.package_type ~= CodesysV3.NameServicePackageTypes.Identification then
|
||||
return false, "The payload in the response isn't a Name Service Identification"
|
||||
end
|
||||
|
||||
-- TODO: we should handle other versions than v4.00 as well
|
||||
if o.version ~= 0x0400 then
|
||||
return false, "The response contained a different version than v4.00 which we don't support right now"
|
||||
end
|
||||
|
||||
stdnse.debug1("Received Codesys V3 Name Service Identification Response packet we can parse")
|
||||
|
||||
-- FIMXE: Intel vs Motorola Byte Order - means: Little vs Big endian. Does endianness of fields in the
|
||||
-- packet later on have different endianness as well? Don't have a Big Endian device to test at hand
|
||||
|
||||
-- Start parsing the PLC identification payload
|
||||
local parentAddrSize, nodeNameLength, deviceNameLength, vendorNameLength,
|
||||
serialNumberLength, oemDataLength
|
||||
|
||||
o.maxChannels, o.intelByteOrder, o.addrDifference, parentAddrSize, pos = string.unpack("<I2 I1 I1 I2", data, pos)
|
||||
nodeNameLength, deviceNameLength, vendorNameLength, pos = string.unpack("<I2 I2 I2", data, pos)
|
||||
o.targetType, o.targetId, o.targetVersion, o.flags, pos = string.unpack("<I4 I4 I4 I4", data, pos)
|
||||
serialNumberLength, oemDataLength, o.blkDrvType, pos = string.unpack("<I1 I1 I1 x xxxx xxxx", data, pos)
|
||||
|
||||
stdnse.debug1("Max Channels: %x - Intel Byte Order: %x - Parent Addr. Difference: %x - Parent Addr. Size: %x", o.maxChannels, o.intelByteOrder, o.addrDifference, parentAddrSize)
|
||||
stdnse.debug1("Node Name Length: %x - Device Name Length: %x - Vendor Name Length: %x", nodeNameLength, deviceNameLength, vendorNameLength)
|
||||
stdnse.debug1("Target Type: %x - Target ID: %x - Target Version: %x - Flags: %x", o.targetType, o.targetId, o.targetVersion, o.flags)
|
||||
stdnse.debug1("Serial Number Length: %x - OEM Data Length: %x - Block Driver Type: %x", serialNumberLength, oemDataLength, o.blkDrvType)
|
||||
|
||||
-- following the structured data, comes variable length data with the sizes we just parsed
|
||||
-- following order should work: parent address, node name, device name, vendor name, serial number, OEM specific data
|
||||
|
||||
o.addrParent = string.sub(data, pos, pos + parentAddrSize)
|
||||
pos = pos + parentAddrSize
|
||||
stdnse.debug1("Parent Address: %s", stdnse.tohex(o.addrParent))
|
||||
|
||||
o.nodeName = string.sub(data, pos, pos + nodeNameLength*2 - 1)
|
||||
o.nodeName = unicode.utf16to8(o.nodeName)
|
||||
stdnse.debug1("Node Name: %s", o.nodeName)
|
||||
pos = pos + nodeNameLength*2 + 2
|
||||
|
||||
o.deviceName = string.sub(data, pos, pos + deviceNameLength*2 - 1)
|
||||
o.deviceName = unicode.utf16to8(o.deviceName)
|
||||
stdnse.debug1("Device Name: %s", stdnse.tohex(o.deviceName))
|
||||
pos = pos + deviceNameLength*2 +2
|
||||
|
||||
o.vendorName = string.sub(data, pos, pos + vendorNameLength*2 - 1)
|
||||
o.vendorName = unicode.utf16to8(o.vendorName)
|
||||
stdnse.debug1("Vendor Name: %s", stdnse.tohex(o.vendorName))
|
||||
pos = pos + vendorNameLength*2 + 2
|
||||
|
||||
o.serialNumber = string.sub(data, pos, pos + serialNumberLength - 1)
|
||||
stdnse.debug1("Serial Number: %s", o.serialNumber)
|
||||
pos = pos + serialNumberLength + 1
|
||||
|
||||
o.oemData = string.sub(data, pos, pos + oemDataLength)
|
||||
pos = pos + oemDataLength
|
||||
stdnse.debug1("OEM data: %s", stdnse.tohex(o.oemData))
|
||||
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
return true, o
|
||||
end,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
version_to_str = ipOps.fromdword
|
||||
|
||||
return _ENV
|
||||
249
scripts/broadcast-codesys-discover.nse
Normal file
249
scripts/broadcast-codesys-discover.nse
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
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
|
||||
-- | 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
|
||||
-- | 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
|
||||
-- | 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-codesys-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 <agalauner@rapid7.com>
|
||||
--
|
||||
|
||||
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:
|
||||
-- <code>name</code> containing the device name
|
||||
-- <code>address</code> containing the device address
|
||||
-- <code>netmask</code> containing the device netmask
|
||||
-- <code>broadcast</code> containing the device broadcast address
|
||||
getInterfaces = function(link, up)
|
||||
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,
|
||||
broadcast = iface.broadcast
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function codesys_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, broadcast = iinfo.broadcast } }
|
||||
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 - Broadcast: %s", iface.name, iface.address, iface.netmask, iface.broadcast)
|
||||
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, 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 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(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)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- wait until all threads are done
|
||||
repeat
|
||||
for thread, info in pairs(threads) do
|
||||
if info() == "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()
|
||||
local ips = stdnse.output_table()
|
||||
|
||||
for _, r in ipairs(result) do
|
||||
local out = stdnse.output_table()
|
||||
|
||||
out["interface"] = r.iface
|
||||
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
|
||||
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
|
||||
end
|
||||
148
scripts/codesys-plc-info.nse
Normal file
148
scripts/codesys-plc-info.nse
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
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 = [[
|
||||
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:
|
||||
-- | 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)
|
||||
|
||||
--
|
||||
-- Version 0.1
|
||||
-- Created 23/06/2021 - v0.1 - created by Andreas Galauner <agalauner@rapid7.com>
|
||||
--
|
||||
|
||||
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)
|
||||
-- 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()
|
||||
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
|
||||
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(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:send(packet)
|
||||
if not status then
|
||||
return fail(string.format("Send 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 status, response = codesys3.CodesysV3.NameServiceResponse:new(data)
|
||||
if ( status ) then
|
||||
result = response
|
||||
|
||||
-- One valid unicast response is enough for us, we can stop receiving more
|
||||
break
|
||||
end
|
||||
end
|
||||
until( not(status) )
|
||||
|
||||
socket:close()
|
||||
|
||||
-- Display the results
|
||||
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)
|
||||
|
||||
return out
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue