mirror of
https://github.com/nmap/nmap.git
synced 2025-12-06 04:31:29 +00:00
Add 4 scripts from the DINA Community
This commit is contained in:
304
scripts/hartip-info.nse
Executable file
304
scripts/hartip-info.nse
Executable file
@@ -0,0 +1,304 @@
|
||||
local shortport = require "shortport"
|
||||
local stdnse = require "stdnse"
|
||||
local string = require "string"
|
||||
local table = require "table"
|
||||
local nmap = require "nmap"
|
||||
local nsedebug = require "nsedebug"
|
||||
|
||||
description = [[
|
||||
This NSE script is used to send a HART-IP packet to a HART device that has TCP 5094 open.
|
||||
The script will establish Session with HART device, then Read Unique Identifier and
|
||||
Read Long Tag packets are sent to parse the required HART device information.
|
||||
Read Sub-Device Identity Summary packet with Sub-Device index 00 01 is sent
|
||||
to request information on Sub-Device, if any available. If the response code
|
||||
differs from 0 (success), the error code is passed as Sub-Device Information.
|
||||
Otherwise, the required Sub-Device information is parsed from response packet.
|
||||
|
||||
Device/Sub-Device Information that is parsed includes Long Tag (user assigned device name),
|
||||
Expanded Device Type, Manufacturer ID, Device ID, Device Revision, Software Revision,
|
||||
HART Protocol Major Revision and Private Label Distributor.
|
||||
|
||||
This script was written based of HART Specifications available at
|
||||
https://www.fieldcommgroup.org/hart-specifications.
|
||||
]]
|
||||
---
|
||||
-- @usage
|
||||
-- nmap <host> -p 5094 --script hartip-info
|
||||
--
|
||||
--
|
||||
-- @output
|
||||
--PORT STATE SERVICE
|
||||
--5094/tcp open hart-ip
|
||||
--| hartip-info:
|
||||
--| Device Information:
|
||||
--| IP Address: 172.16.10.90
|
||||
--| Long Tag: ????????????????????????????????
|
||||
--| Expanded Device Type: GW PL ETH/UNI-BUS
|
||||
--| Manufacturer ID: Phoenix Contact
|
||||
--| Device ID: dd4ee3
|
||||
--| Device Revision: 1
|
||||
--| Software Revision: 1
|
||||
--| HART Protocol Major Revision: 7
|
||||
--| Private Label Distributor: Phoenix Contact
|
||||
--| Sub-Device Information:
|
||||
--|_ Error Code: 2
|
||||
-- @xmloutput
|
||||
--<elem key="IP Address">172.16.10.90</elem>
|
||||
--<elem>Long Tag: ????????????????????????????????</elem>
|
||||
--<elem>Expanded Device Type: GW PL ETH/UNI-BUS</elem>
|
||||
--<elem>Manufacturer ID: Phoenix Contact</elem>
|
||||
--<elem>Device ID: dd4ee3</elem>
|
||||
--<elem>Device Revision: 1</elem>
|
||||
--<elem>Software Revision: 1</elem>
|
||||
--<elem>HART Protocol Major Revision: 7</elem>
|
||||
--<elem>Private Label Distributor: Phoenix Contact</elem>
|
||||
|
||||
author = "DINA-community"
|
||||
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
|
||||
categories = {"discovery", "intrusive"}
|
||||
|
||||
-- Function to define the portrule as per nmap standards
|
||||
portrule = shortport.port_or_service(5094, "hart-ip", "tcp")
|
||||
|
||||
-- Table to look up the Product Name based on number-represented Expanded Device Type Code
|
||||
-- Returns "Unknown Device Type" if Expanded Device Type not recognized
|
||||
-- Table data from Common Tables Specification, HCF_SPEC-183, FCG TS20183, Revision 26.0
|
||||
-- 5.1 Table 1. Expanded Device Type Codes
|
||||
|
||||
-- @key expdevtypnum number-represented Device Type Code parsed out of the HART-IP packet
|
||||
local productName = {
|
||||
[4560] = "iTEMP TMT72",
|
||||
[45075] = "GW PL ETH/UNI-BUS",
|
||||
}
|
||||
|
||||
--return device type information
|
||||
local function expdevtyp_lookup(expdevtypnum)
|
||||
return productName[expdevtypnum] or "Unknown Device Type"
|
||||
end
|
||||
|
||||
-- Table to look up the Manufacturer Name based on Manufacturer ID
|
||||
-- Returns "Unknown Manufacturer" if Manufacturer ID not recognized
|
||||
-- Table data from Common Tables Specification, HCF_SPEC-183, FCG TS20183, Revision 26.0
|
||||
-- 5.8 Table 8. Manufacturer Identification Codes
|
||||
|
||||
-- @key manidnum number-represented Manufacturer ID parsed out of the HART-IP packet
|
||||
local manufacturerName = {
|
||||
[176] = "Phoenix Contact",
|
||||
}
|
||||
|
||||
--return manufacturer information
|
||||
local function manid_lookup(manidnum)
|
||||
return manufacturerName[manidnum] or "Unknown Manufacturer"
|
||||
end
|
||||
|
||||
-- Action Function that is used to run the NSE. This function will send
|
||||
-- the initial query to the host and port that were passed in via nmap.
|
||||
--
|
||||
-- @param host Host that was scanned via nmap
|
||||
-- @param port port that was scanned via nmap
|
||||
action = function(host,port)
|
||||
-- create local vars for socket handling
|
||||
local socket, try, catch, status, err
|
||||
|
||||
-- create new socket
|
||||
socket = nmap.new_socket()
|
||||
|
||||
-- set timeout
|
||||
socket:set_timeout(stdnse.get_timeout(host))
|
||||
|
||||
-- define the catch of the try statement
|
||||
catch = function()
|
||||
socket:close()
|
||||
end
|
||||
|
||||
-- create new try
|
||||
try = nmap.new_try(catch)
|
||||
|
||||
-- connect to port on host
|
||||
try(socket:connect(host, port))
|
||||
stdnse.debug(1, "#- socket connection established.")
|
||||
|
||||
-- send the initiate session packet
|
||||
-- receive response
|
||||
-- abort if no response
|
||||
local sessInitQuery = stdnse.fromhex("010000000001000D0100004E20")
|
||||
try(socket:send(sessInitQuery))
|
||||
|
||||
local rcvstatus, response = socket:receive()
|
||||
if(rcvstatus == false) then
|
||||
stdnse.debug(1, "#- session initiation with HART device - FAIL.")
|
||||
return nil
|
||||
end
|
||||
stdnse.debug(1, "#- session initiation with HART device - SUCCESS.")
|
||||
|
||||
-- Command 0 BEGIN --
|
||||
|
||||
-- send the Command 0 Read Unique Idenifier packet
|
||||
-- receive response, abort if no response
|
||||
local cmd0Req = stdnse.fromhex("010003000002000D0280000082")
|
||||
try(socket:send(cmd0Req))
|
||||
|
||||
local rcvstatus, response = socket:receive()
|
||||
if(rcvstatus == false) then
|
||||
stdnse.debug(1, "#- command 0 Read Unique Identifier request - FAIL.")
|
||||
return nil
|
||||
end
|
||||
stdnse.debug(1, "#- command 0 Read Unique Identifier request - SUCCESS.")
|
||||
|
||||
-- get hart-ip version, message type, message id, response status and sequence number
|
||||
-- abort if no response
|
||||
local _, _, _, res_status, _ = string.unpack(">BBBBI2", response, 1)
|
||||
if (res_status ~= 0) then
|
||||
stdnse.debug(1, "#- command 0 Read Unique Identifier response - FAIL.")
|
||||
return nil
|
||||
end
|
||||
stdnse.debug(1, "#- command 0 Read Unique Identifier response - SUCCESS.")
|
||||
|
||||
--- unpack device information from Command 0
|
||||
|
||||
-- create table for output and device information
|
||||
local output = stdnse.output_table()
|
||||
local deviceInfo = stdnse.output_table()
|
||||
|
||||
deviceInfo["IP Address"] = host.ip
|
||||
|
||||
-- get expanded device type
|
||||
-- lookup device type number
|
||||
local expDevTypeNum, Index = string.unpack(">I2", response, 16)
|
||||
local expandedDeviceType = expdevtyp_lookup(expDevTypeNum)
|
||||
deviceInfo["Expanded Device Type"] = expandedDeviceType
|
||||
|
||||
-- get master-to-slave minimum preambles
|
||||
local minPreMasterToSlave, Index = string.unpack("B", response, Index)
|
||||
|
||||
-- get HART protocol major revision number
|
||||
local hartProtocolMajorRevision, Index = string.unpack("B", response, Index)
|
||||
deviceInfo["HART Protocol Major Revision"] = hartProtocolMajorRevision
|
||||
|
||||
-- get device revision number
|
||||
local deviceRevision, Index = string.unpack("B",response, Index)
|
||||
deviceInfo["Device Revision"] = deviceRevision
|
||||
|
||||
-- get software revision number
|
||||
local softwareRevision, Index = string.unpack("B",response, Index)
|
||||
deviceInfo["Software Revision"] = softwareRevision
|
||||
|
||||
-- get hardware revision level with physical signaling code and flags
|
||||
local _,flags, Index = string.unpack("BB", response, Index)
|
||||
|
||||
-- get device ID in hex format
|
||||
local deviceID, Index = string.unpack("c3", response, Index)
|
||||
deviceID = stdnse.tohex(deviceID)
|
||||
deviceInfo["Device ID"] = deviceID
|
||||
|
||||
-- get slave-to-master minimum preambles, last device variable code,
|
||||
-- configuration change counter, extended field device status
|
||||
_,_,_,_, Index = string.unpack("BBI2B", response, Index)
|
||||
|
||||
-- get manufacturer ID
|
||||
-- lookup manufacturer id
|
||||
local manufacturerID, Index = string.unpack(">I2", response, Index)
|
||||
manufacturerID = manid_lookup(manufacturerID)
|
||||
deviceInfo["Manufacturer ID"] = manufacturerID
|
||||
|
||||
-- get private label distributor
|
||||
-- lookup manufacturer id
|
||||
local privateLabelDistributor, Index = string.unpack(">I2", response, Index)
|
||||
privateLabelDistributor = manid_lookup(privateLabelDistributor)
|
||||
deviceInfo["Private Label Distributor"] = privateLabelDistributor
|
||||
|
||||
-- get device profile
|
||||
local deviceProfile = string.unpack("B", response, Index)
|
||||
|
||||
-- Command 0 END --
|
||||
|
||||
-- Command 20 BEGIN --
|
||||
|
||||
local longAddress = stdnse.tohex(expDevTypeNum) .. deviceID
|
||||
|
||||
-- send the Command 20 Read Long Tag packet
|
||||
-- receive response, abort if no response
|
||||
local cmd20Req = stdnse.fromhex("010003000003001182" .. longAddress .. "140045")
|
||||
try(socket:send(cmd20Req))
|
||||
|
||||
local rcvstatus, response = socket:receive()
|
||||
if(rcvstatus == false) then
|
||||
stdnse.debug(1, "#- command 20 Read Long Tag request - FAIL.")
|
||||
output['Device Information'] = deviceInfo
|
||||
return output
|
||||
end
|
||||
stdnse.debug(1, "#- command 20 Read Long Tag request - SUCCESS.")
|
||||
|
||||
-- get hart-ip version, message type, message id, response status and sequence number
|
||||
-- abort if no response
|
||||
local _, _, _, res_status, _ = string.unpack(">BBBBI2", response, 1)
|
||||
if (res_status ~= 0) then
|
||||
stdnse.debug(1, "#- command 20 Read Long Tag response - FAIL.")
|
||||
output['Device Information'] = deviceInfo
|
||||
return output
|
||||
end
|
||||
stdnse.debug(1, "#- command 20 Read Long Tag response - SUCCESS.")
|
||||
|
||||
--- unpack device information from Command 20
|
||||
|
||||
-- get device long tag
|
||||
local longTag = string.unpack("c32", response, 19)
|
||||
stdnse.debug(1, "#--- Long Tag = " .. longTag)
|
||||
deviceInfo["Long Tag"] = longTag
|
||||
|
||||
-- Command 20 END --
|
||||
|
||||
--- Command 84 BEGIN
|
||||
|
||||
-- send the Command 84 Read Sub-Device Identity Summary packet for Sub-Device at index 0001
|
||||
-- receive response, abort if no response
|
||||
local subDeviceIndex = "0001"
|
||||
local cmd84Req = stdnse.fromhex("010003000004001382" .. longAddress .. "5402" .. subDeviceIndex .. "06")
|
||||
try(socket:send(cmd84Req))
|
||||
|
||||
local rcvstatus, response = socket:receive()
|
||||
if(rcvstatus == false) then
|
||||
stdnse.debug(1, "#- command 84 Read Sub-Device Identity Summary request - FAIL.")
|
||||
output['Device Information'] = deviceInfo
|
||||
return output
|
||||
end
|
||||
stdnse.debug(1, "#- command 84 Read Sub-Device Identity Summary request - SUCCESS.")
|
||||
|
||||
-- get hart-ip version, message type, message id, response status and sequence number
|
||||
-- abort if no response
|
||||
local _, _, _, res_status, _ = string.unpack(">BBBBI2", response, 1)
|
||||
if (res_status ~= 0) then
|
||||
stdnse.debug(1, "#- command 84 Read Sub-Device Identity Summary response - FAIL.")
|
||||
output['Device Information'] = deviceInfo
|
||||
return output
|
||||
end
|
||||
stdnse.debug(1, "#- command 84 Read Sub-Device Identity Summary response - SUCCESS.")
|
||||
|
||||
--- get sub-device information from Command 84
|
||||
local subDeviceInfo = stdnse.output_table()
|
||||
|
||||
-- get response code
|
||||
-- abort if no success
|
||||
local responseCode = string.unpack("B", response, 17)
|
||||
if (responseCode ~= 0) then
|
||||
stdnse.debug(1, "#- command 84 Read Sub-Device Identity Summary response code %d - FAIL.", responseCode)
|
||||
subDeviceInfo["Error Code"] = responseCode
|
||||
|
||||
output['Device Information'] = deviceInfo
|
||||
output['Sub-Device Information'] = subDeviceInfo
|
||||
return output
|
||||
end
|
||||
|
||||
--- Command 84 END
|
||||
|
||||
-- close socket
|
||||
socket:close()
|
||||
stdnse.debug(1, "#- socket connection terminated.")
|
||||
|
||||
-- populate output table
|
||||
output['Device Information'] = deviceInfo
|
||||
output['Sub-Device Information'] = subDeviceInfo
|
||||
|
||||
-- return output table to Nmap
|
||||
return output
|
||||
end
|
||||
380
scripts/iec61850-mms.nse
Executable file
380
scripts/iec61850-mms.nse
Executable file
@@ -0,0 +1,380 @@
|
||||
local iec61850mms = require "iec61850mms"
|
||||
local nmap = require "nmap"
|
||||
local shortport = require "shortport"
|
||||
local stdnse = require "stdnse"
|
||||
local string = require "string"
|
||||
local table = require "table"
|
||||
|
||||
description = [[
|
||||
Queries a IEC 61850-8-1 MMS server. Sends Initate-Request, Identify-Request and Read-Request to LN0 and LPHD.
|
||||
|
||||
Output contains following attributes:
|
||||
|
||||
* modelName_identify: Identify-Response attribute model_name
|
||||
* vendorName_identify: Identify-Response attribute vendor_name
|
||||
* modelNumber_identify: Identify-Response attribute revision
|
||||
* productFamily: Read-Response attribute 'LLN0$DC$NamPlt$d'
|
||||
* configuration: Read-Response attribute 'LLN0$DC$NamPlt$configRev'
|
||||
* vendorName: Read-Response attribute 'LPHD$DC$PhyNam$vendor' (old: 'LLN0$DC$NamPlt$vendor')
|
||||
* serialNumber: Read-Response attribute 'LPHD$DC$PhyNam$serNum'
|
||||
* modelNumber: Read-Response attribute 'LPHD$DC$PhyNam$model'
|
||||
* firmwareVersion: Read-Response attribute 'LPHD$DC$PhyNam$swRev' (old: 'LLN0$DC$NamPlt$swRev')
|
||||
]]
|
||||
|
||||
---
|
||||
-- @usage
|
||||
-- nmap --script iec61850-mms.nse -p 102 <target>
|
||||
--
|
||||
|
||||
---
|
||||
-- @output
|
||||
-- 102/tcp open iso-tsap
|
||||
--| iec61850_mms.nse:
|
||||
--| modelName_identify: MMS-LITE-80X-001
|
||||
--| productFamily: High End Meter
|
||||
--| vendorName: Schneider Electric
|
||||
--| vendorName_identify: SISCO
|
||||
--| serialNumber: ME-1810A424-02
|
||||
--| modelNumber: 8000
|
||||
--| modelNumber_identify: 6.0000.3
|
||||
--| firmwareVersion: 001.004.003
|
||||
--|_ configuration: 2022-08-19 08:27:20
|
||||
|
||||
|
||||
author = "Dennis Rösch, Max Helbig"
|
||||
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
|
||||
categories = {"discovery", "intrusive", "version"}
|
||||
|
||||
-- Helpers
|
||||
function replaceEmptyStrings(tbl)
|
||||
for key, value in pairs(tbl) do
|
||||
if type(value) == "table" then
|
||||
replaceEmptyStrings(value)
|
||||
elseif type(value) == "string" and value == "" then
|
||||
tbl[key] = "<EMPTY_STRING>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function searchTable(searchString, myTable)
|
||||
local matches = {}
|
||||
local uniqueEntries = {}
|
||||
local extractedPart
|
||||
for i, entry in ipairs(myTable) do
|
||||
if string.find(entry, searchString) then
|
||||
local dollarIndex = string.find(entry, "%$")
|
||||
if not dollarIndex then
|
||||
extractedPart = entry
|
||||
else
|
||||
extractedPart = string.sub(entry, 1, dollarIndex - 1)
|
||||
end
|
||||
if not uniqueEntries[extractedPart] then
|
||||
uniqueEntries[extractedPart] = true
|
||||
table.insert(matches, extractedPart)
|
||||
end
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
-- Rules
|
||||
portrule = shortport.portnumber(102, "iso-tsap")
|
||||
|
||||
-- Actions
|
||||
action = function(host, port)
|
||||
local timeout = 500
|
||||
|
||||
local status, recv
|
||||
local output = {}
|
||||
local socket = nmap.new_socket()
|
||||
|
||||
local decoder = iec61850mms.MMSDecoder:new()
|
||||
local encoder = iec61850mms.MMSEncoder:new()
|
||||
local query = iec61850mms.MMSQueries:new()
|
||||
|
||||
socket:set_timeout(timeout)
|
||||
|
||||
stdnse.debug(2, "Connecting to host")
|
||||
status, recv = socket:connect(host, port, "tcp")
|
||||
if not status then
|
||||
return nil
|
||||
end
|
||||
stdnse.debug(2, "Connected")
|
||||
|
||||
stdnse.debug(2, "Sending CR_TPDU")
|
||||
local CR_TPDU = "\x03\x00\x00\x16\x11\xe0\x00\x00\x00\x01\x00\xc1\x02\x00\x00\xc2\x02\x00\x01\xc0\x01\x0a"
|
||||
status = socket:send( CR_TPDU )
|
||||
if not status then
|
||||
return nil
|
||||
end
|
||||
status, recv = socket:receive_bytes(1024)
|
||||
stdnse.debug(2, "Response recieved")
|
||||
stdnse.debug(3, "cr_tpdu: %s", stdnse.tohex(recv) )
|
||||
|
||||
|
||||
local MMS_INITIATE = "\x03\x00\x00\xd3\x02\xf0\x80\x0d\xca\x05\x06\x13\x01\x00\x16\x01\x02\x14\x02\x00\x02\x33\x02" ..
|
||||
"\x00\x01\x34\x02\x00\x01\xc1\xb4\x31\x81\xb1\xa0\x03\x80\x01\x01" ..
|
||||
"\xa2\x81\xa9\x81\x04\x00\x00\x00\x01\x82\x04\x00\x00\x00\x01\xa4" ..
|
||||
"\x23\x30\x0f\x02\x01\x01\x06\x04\x52\x01\x00\x01\x30\x04\x06\x02" ..
|
||||
"\x51\x01\x30\x10\x02\x01\x03\x06\x05\x28\xca\x22\x02\x01\x30\x04" ..
|
||||
"\x06\x02\x51\x01\x61\x76\x30\x74\x02\x01\x01\xa0\x6f\x60\x6d\xa1" ..
|
||||
"\x07\x06\x05\x28\xca\x22\x02\x03\xa2\x07\x06\x05\x29\x01\x87\x67" ..
|
||||
"\x01\xa3\x03\x02\x01\x0c\xa4\x03\x02\x01\x00\xa5\x03\x02\x01\x00" ..
|
||||
"\xa6\x06\x06\x04\x29\x01\x87\x67\xa7\x03\x02\x01\x0c\xa8\x03\x02" ..
|
||||
"\x01\x00\xa9\x03\x02\x01\x00\xbe\x33\x28\x31\x06\x02\x51\x01\x02" ..
|
||||
"\x01\x03\xa0\x28\xa8\x26\x80\x03\x00\xfd\xe8\x81\x01\x0a\x82\x01" ..
|
||||
"\x0a\x83\x01\x05\xa4\x16\x80\x01\x01\x81\x03\x05\xf1\x00\x82\x0c" ..
|
||||
"\x03\xee\x1c\x00\x00\x00\x00\x00\x00\x00\xed\x18"
|
||||
|
||||
stdnse.debug(2, "Sending MMS initiate")
|
||||
status = socket:send( MMS_INITIATE )
|
||||
if not status then
|
||||
return nil
|
||||
end
|
||||
status, recv = socket:receive_bytes(1024)
|
||||
stdnse.debug(2, "Response recieved")
|
||||
stdnse.debug(3, "mms_initiate: %s", stdnse.tohex(recv) )
|
||||
|
||||
local MMS_IDENTIFY = "\x03\x00\x00\x1b\x02\xf0\x80\x01\x00\x01\x00\x61\x0e\x30\x0c\x02" ..
|
||||
"\x01\x03\xa0\x07\xa0\x05\x02\x01\x01\x82\x00"
|
||||
|
||||
stdnse.debug(2, "Sending MMS identify")
|
||||
status = socket:send( MMS_IDENTIFY )
|
||||
if not status then
|
||||
return nil
|
||||
end
|
||||
status, recv = socket:receive_bytes(2048)
|
||||
stdnse.debug(2, "Response recieved")
|
||||
stdnse.debug(3, "mms_identify: %s", stdnse.tohex(recv) )
|
||||
|
||||
local output = stdnse.output_table()
|
||||
|
||||
if ( status and recv ) then
|
||||
local mmsIdentstruct = decoder:unpackAndDecode(recv)
|
||||
if not mmsIdentstruct then
|
||||
stdnse.debug(1, "error while decoding")
|
||||
return output
|
||||
end
|
||||
replaceEmptyStrings(mmsIdentstruct)
|
||||
|
||||
local vendor_name = mmsIdentstruct.confirmed_ResponsePDU.identify.vendorName
|
||||
local model_name = mmsIdentstruct.confirmed_ResponsePDU.identify.modelName
|
||||
local revision = mmsIdentstruct.confirmed_ResponsePDU.identify.revision
|
||||
|
||||
stdnse.debug(1, "vendor_name: %s", vendor_name )
|
||||
stdnse.debug(1, "model_name: %s", model_name )
|
||||
stdnse.debug(1, "revision: %s", revision )
|
||||
output["modelName_identify"] = model_name
|
||||
output["vendorName_identify"] = vendor_name
|
||||
output["modelNumber_identify"] = revision
|
||||
else
|
||||
return nil
|
||||
end
|
||||
|
||||
local invokeID = 1
|
||||
|
||||
|
||||
|
||||
local vmd_NameList_Struct = query:nameList(invokeID)
|
||||
local MMS_GETNAMELIST_vmdspecific = encoder:packmmsInTPKT(encoder:mmsPDU(vmd_NameList_Struct))
|
||||
stdnse.debug(2, "Sending MMS getNameList (vmdSpecific)")
|
||||
status = socket:send( MMS_GETNAMELIST_vmdspecific )
|
||||
if not status then
|
||||
stdnse.debug(1, "error while sending MMS getNameList (vmdSpecific)")
|
||||
return output
|
||||
end
|
||||
|
||||
status, recv = socket:receive_bytes(1024)
|
||||
stdnse.debug(2, "Response recieved")
|
||||
stdnse.debug(3, "mms_getnamelist: %s", stdnse.tohex(recv) )
|
||||
|
||||
local vmd_names
|
||||
if ( status and recv ) then
|
||||
local mmsNLTab = decoder:unpackAndDecode(recv)
|
||||
if not mmsNLTab then
|
||||
stdnse.debug(1, "error while decoding")
|
||||
return output
|
||||
end
|
||||
vmd_names = mmsNLTab.confirmed_ResponsePDU.getNameList.listOfIdentifier
|
||||
stdnse.debug(1, "found %d vmdNames", #vmd_names )
|
||||
for i, v in ipairs(vmd_names) do
|
||||
stdnse.debug(1, "vmd_name %d: %s", i, v )
|
||||
end
|
||||
else
|
||||
stdnse.debug(1, "error while processing MMS getNameList (vmdSpecific) response")
|
||||
return output
|
||||
end
|
||||
|
||||
-- reading complete vmdspecific NameList
|
||||
|
||||
|
||||
|
||||
local matches
|
||||
local vmd_name
|
||||
stdnse.debug(2, "Start reading complete NameList")
|
||||
for i, v in ipairs(vmd_names) do
|
||||
local morefollows = true
|
||||
local continueAfter = ""
|
||||
local allIdentifiers = {}
|
||||
stdnse.debug(2, "get NameList for vmdName %s", v)
|
||||
while morefollows do
|
||||
local mmsStruct = query:nameList(invokeID, v, continueAfter)
|
||||
local sendString = encoder:packmmsInTPKT(encoder:mmsPDU(mmsStruct))
|
||||
stdnse.debug(2, "Sending getNameList request")
|
||||
status = socket:send( sendString )
|
||||
if not status then
|
||||
stdnse.debug(1, "error sending request")
|
||||
return output
|
||||
end
|
||||
|
||||
status, recv = socket:receive_bytes(100000)
|
||||
stdnse.debug(2, "Response recieved")
|
||||
stdnse.debug(3, "mms_getnamelist recv: %s", stdnse.tohex(recv) )
|
||||
if ( status and recv ) then
|
||||
local recv_Struct = decoder:unpackAndDecode(recv)
|
||||
if not recv_Struct then
|
||||
stdnse.debug(1, "error while decoding")
|
||||
return output
|
||||
end
|
||||
|
||||
local identifier = recv_Struct.confirmed_ResponsePDU.getNameList.listOfIdentifier
|
||||
for i, v in ipairs(identifier) do table.insert(allIdentifiers, v) end
|
||||
if #identifier > 100 then
|
||||
stdnse.debug(1, "Response contains more then 100 identifiers")
|
||||
stdnse.debug(2, "Just got %d identifiers", #identifier)
|
||||
end
|
||||
|
||||
morefollows = recv_Struct.confirmed_ResponsePDU.getNameList.moreFollows
|
||||
if morefollows then
|
||||
continueAfter = identifier[#identifier]
|
||||
stdnse.debug(2, "More identifiers availible!")
|
||||
end
|
||||
|
||||
invokeID = invokeID + 1
|
||||
else
|
||||
stdnse.debug(1, "error while processing MMS getNameList response")
|
||||
return output
|
||||
end
|
||||
end
|
||||
stdnse.debug(2, "Reading complete NameList done")
|
||||
|
||||
stdnse.debug(2, "Searching for LPHD in %d identifiers", #allIdentifiers)
|
||||
matches = searchTable("LPHD", allIdentifiers)
|
||||
if #matches >= 1 then
|
||||
vmd_name = v
|
||||
break
|
||||
end
|
||||
end -- for loop
|
||||
stdnse.debug(2, "Searching done: found %d unique entrys", #matches)
|
||||
|
||||
|
||||
if #matches == 0 then
|
||||
stdnse.debug(1, "No Logical Node contains LPHD")
|
||||
end
|
||||
|
||||
if #matches > 1 then
|
||||
stdnse.debug(1, "Found more then one Node")
|
||||
return output
|
||||
end
|
||||
|
||||
|
||||
|
||||
local attributes = {
|
||||
'LLN0$DC$NamPlt$d',
|
||||
'LLN0$DC$NamPlt$configRev'
|
||||
}
|
||||
|
||||
local Node_Ready = false
|
||||
local node
|
||||
if #matches == 1 then
|
||||
node = matches[1]
|
||||
Node_Ready = true
|
||||
stdnse.debug(2, "Node is: %s", node)
|
||||
table.insert(attributes, node .. '$DC$PhyNam$vendor')
|
||||
table.insert(attributes, node .. '$DC$PhyNam$serNum')
|
||||
table.insert(attributes, node .. '$DC$PhyNam$model')
|
||||
table.insert(attributes, node .. '$DC$PhyNam$swRev')
|
||||
end
|
||||
|
||||
local mmsRequest = query:askfor(invokeID, vmd_name, attributes)
|
||||
local MMS_READREQUEST = encoder:packmmsInTPKT(mmsRequest)
|
||||
|
||||
stdnse.debug(2, "Sending MMS readRequest")
|
||||
status = socket:send( MMS_READREQUEST )
|
||||
if not status then
|
||||
return nil
|
||||
end
|
||||
|
||||
status, recv = socket:receive_bytes(1024)
|
||||
stdnse.debug(2, "Response recieved")
|
||||
stdnse.debug(3, "mms_read: %s", stdnse.tohex(recv) )
|
||||
|
||||
local mmsstruct
|
||||
if ( status and recv ) then
|
||||
mmsstruct = decoder:unpackAndDecode(recv)
|
||||
if not mmsstruct then
|
||||
stdnse.debug(1, "error while decoding")
|
||||
return output
|
||||
end
|
||||
replaceEmptyStrings(mmsstruct)
|
||||
else
|
||||
stdnse.debug(1, "error while processing MMS getNameList response")
|
||||
return output
|
||||
end
|
||||
|
||||
local mmsoutput
|
||||
local attNum = #attributes
|
||||
local rplNum = #mmsstruct.confirmed_ResponsePDU.Read_Response.listOfAccessResult
|
||||
if rplNum == attNum then
|
||||
mmsoutput = mmsstruct.confirmed_ResponsePDU.Read_Response.listOfAccessResult
|
||||
else
|
||||
|
||||
stdnse.debug(2,"\nReply from Host %s at port %d was not compliant with standard", host["ip"], port["number"])
|
||||
stdnse.debug(2,"Request for %d attributes has been replied with %d values", attNum, rplNum)
|
||||
stdnse.debug(2,"attempting individual queries...\n")
|
||||
mmsoutput = {}
|
||||
for i = 1, attNum do
|
||||
local mmsRequest = query:askfor(i, vmd_name, attributes[i])
|
||||
local MMS_READREQUEST = encoder:packmmsInTPKT(mmsRequest)
|
||||
|
||||
status = socket:send( MMS_READREQUEST )
|
||||
if not status then
|
||||
return nil
|
||||
end
|
||||
|
||||
status, recv = socket:receive_bytes(1024)
|
||||
stdnse.debug(1, "mms_read recv: %s", stdnse.tohex(recv) )
|
||||
|
||||
if ( status and recv ) then
|
||||
local mmsstruct = decoder:unpackAndDecode(recv)
|
||||
if not mmsstruct then
|
||||
stdnse.debug(1, "error while decoding")
|
||||
return output
|
||||
end
|
||||
replaceEmptyStrings(mmsstruct)
|
||||
table.insert(mmsoutput, {})
|
||||
mmsoutput[i][1] = mmsstruct.confirmed_ResponsePDU.Read_Response.listOfAccessResult[1][1]
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- create table for output
|
||||
output["productFamily"] = mmsoutput[1][1]
|
||||
output["configuration"] = mmsoutput[2][1]
|
||||
|
||||
if Node_Ready then
|
||||
output["vendorName"] = mmsoutput[3][1]
|
||||
output["serialNumber"] = mmsoutput[4][1]
|
||||
output["modelNumber"] = mmsoutput[5][1]
|
||||
output["firmwareVersion"] = mmsoutput[6][1]
|
||||
else
|
||||
output["vendorName"] = "<NO_LPHD_FOUND>"
|
||||
output["serialNumber"] = "<NO_LPHD_FOUND>"
|
||||
output["modelNumber"] = "<NO_LPHD_FOUND>"
|
||||
output["firmwareVersion"] = "<NO_LPHD_FOUND>"
|
||||
end
|
||||
return output
|
||||
|
||||
end
|
||||
439
scripts/multicast-profinet-discovery.nse
Executable file
439
scripts/multicast-profinet-discovery.nse
Executable file
@@ -0,0 +1,439 @@
|
||||
local coroutine = require "coroutine"
|
||||
local nmap = require "nmap"
|
||||
local stdnse = require "stdnse"
|
||||
local string = require "string"
|
||||
local table = require "table"
|
||||
local packet = require "packet"
|
||||
local ipOps = require "ipOps"
|
||||
|
||||
|
||||
description = [[
|
||||
Sends a multicast PROFINET DCP Identify All message and prints the responses.
|
||||
|
||||
Reference:
|
||||
* https://profinetuniversity.com/naming-addressing/profinet-dcp/
|
||||
]]
|
||||
|
||||
---@output
|
||||
--multicast-profinet-discovery:
|
||||
--| devices:
|
||||
--|
|
||||
--| 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
|
||||
|
||||
author = "Stefan Eiwanger, DINA-community"
|
||||
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"}
|
||||
|
||||
prerule = function()
|
||||
if not nmap.is_privileged() then
|
||||
stdnse.debug(1, "Nmap is NOT running as privileged.")
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local pn_dcp_multicast = "01:0e:cf:00:00:00"
|
||||
|
||||
|
||||
-- generate raw profinet identify all message
|
||||
--@param iface interface table containing mac address
|
||||
--@return eth_packet ethernet packet for sending over socket
|
||||
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 dest_mac = packet.mactobin(pn_dcp_multicast)
|
||||
local eth_proto = string.pack("I2", 0x9288)
|
||||
|
||||
-- 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)
|
||||
|
||||
-- build the packet
|
||||
eth_packet = dest_mac .. src_mac .. eth_proto .. blockData
|
||||
local length = string.len(eth_packet)
|
||||
|
||||
-- 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
|
||||
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)
|
||||
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)
|
||||
|
||||
-- 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
|
||||
|
||||
-- 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
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
-- get all possible interfaces
|
||||
--@param link type of interface e.g. "ethernet"
|
||||
--@param up status of the interface
|
||||
--@return result table with all interfaces which match the given requirements
|
||||
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.mac ) then
|
||||
if #result == 0 then
|
||||
table.insert(result, iface)
|
||||
else
|
||||
local exists = false
|
||||
for _, intface in ipairs(result) do
|
||||
if intface.mac == iface.mac then
|
||||
exists = true
|
||||
end
|
||||
end
|
||||
if not exists then
|
||||
table.insert(result, iface)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- helpfunction for thread call
|
||||
--@param iface interface table
|
||||
--@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)
|
||||
local condvar = nmap.condvar(devices)
|
||||
local dnet = nmap.new_dnet()
|
||||
local pcap_s = nmap.new_socket()
|
||||
pcap_s:set_timeout(2000)
|
||||
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
|
||||
|
||||
status = true
|
||||
while status do
|
||||
status, length, ethData, pn_data = pcap_s:pcap_receive()
|
||||
|
||||
if(status) then
|
||||
devices[#devices + 1] = parse_pndcp(ethData, pn_data)
|
||||
end
|
||||
end
|
||||
dnet:ethernet_close(iface.device); -- close the sender
|
||||
|
||||
|
||||
|
||||
pcap_s:close(iface.device)
|
||||
condvar "signal"
|
||||
return 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 interface_e = nmap.get_interface()
|
||||
local interfaces = {}
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if(interface_e) then -- interface supplied with -e
|
||||
local iface = nmap.get_interface_info(interface_e)
|
||||
if not (iface and iface.link == 'ethernet') then
|
||||
stdnse.debug(1, "%s not supported with %s", iface, SCRIPT_NAME)
|
||||
return false
|
||||
end
|
||||
table.insert(interfaces, iface)
|
||||
else -- discover interfaces
|
||||
interfaces = getInterfaces("ethernet", "up")
|
||||
end
|
||||
|
||||
-- check if at least one interface is available
|
||||
if #interfaces == 0 then
|
||||
print("No interfaces found")
|
||||
return false
|
||||
end
|
||||
|
||||
-- get the frame we want to send
|
||||
|
||||
|
||||
local threads = {}
|
||||
|
||||
local condvar = nmap.condvar(output_tab.devices)
|
||||
|
||||
|
||||
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)
|
||||
threads[co] = true
|
||||
end
|
||||
|
||||
-- wait for all threads to finish sniffing
|
||||
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
|
||||
|
||||
-- check the output if something is doubled there
|
||||
if #output_tab.devices == 0 then
|
||||
print("No profinet devices in the subnet")
|
||||
return 0
|
||||
end
|
||||
|
||||
|
||||
return output_tab
|
||||
|
||||
end
|
||||
166
scripts/profinet-cm-lookup.nse
Executable file
166
scripts/profinet-cm-lookup.nse
Executable file
@@ -0,0 +1,166 @@
|
||||
local nmap = require "nmap"
|
||||
local stdnse = require "stdnse"
|
||||
local shortport = require "shortport"
|
||||
local string = require "string"
|
||||
|
||||
description = [[
|
||||
Sends a DCERPC EPM Lookup Request to PROFINET devices. the DCE/RPC Endpoint Mapper (EPM) targeting Profinet Devices.
|
||||
|
||||
Profinet Devices support the udp-based PNIO-CM protocol under port 34964.
|
||||
PNIO-CM uses DCE/RPC as its underlying protocol.
|
||||
|
||||
|
||||
Profinet Devices support a DCE/RPC UUID Entity under the UUID variant
|
||||
'dea00001-6c97-11d1-8271-00a02442df7d'. This script sends the Lookup Request for this UUID.
|
||||
|
||||
References:
|
||||
* https://rt-labs.com/docs/p-net/profinet_details.html#dce-rpc-uuid-entities
|
||||
* https://wiki.wireshark.org/EPM
|
||||
]]
|
||||
|
||||
---
|
||||
-- @usage nmap -sU <target_ip> -p 34964 --script profinet-cm-lookup
|
||||
---
|
||||
-- @output
|
||||
--PORT STATE SERVICE REASON
|
||||
--34964/udp open|filtered profinet-cm no-response
|
||||
--| profinet-cm-lookup:
|
||||
--| ipAddress: 192.168.10.12
|
||||
--| annotationOffset: 0
|
||||
--| annotationLength: 64
|
||||
--|_ annotation: S7-1500 6ES7 672-5DC01-0YA0 0 V 2 1 7
|
||||
-- @xmloutput
|
||||
--<elem key="ipAddress">192.168.10.12</elem>
|
||||
--<elem key="annotationOffset">0</elem>
|
||||
--<elem key="annotationLength">64</elem>
|
||||
--<elem key="annotation">S7-1500 6ES7 672-5DC01-0YA0 0 V 2 1 7</elem>
|
||||
|
||||
categories = {"discovery", "intrusive"}
|
||||
author = "DINA-community"
|
||||
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
|
||||
|
||||
local EPM_UDP_PORT = 34964
|
||||
|
||||
local DCE_RPC_REQUEST = string.char(
|
||||
0x04,0x00,0x20,0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x83,0xaf,0xe1,0x1f,0x5d,0xc9,0x11,
|
||||
0x91,0xa4,0x08,0x00,0x2b,0x14,0xa0,0xfa,0x01,0x00,0x00,0x00,0x01,0x00,0x01,0x00,
|
||||
0x01,0x00,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,
|
||||
0x0c,0x00,0x00,0x00,0x02,0x00,0xff,0xff,0xff,0xff,0x4c,0x00,0x00,0x00,0x00,0x00)
|
||||
|
||||
local EPM_Lookup = string.char(
|
||||
0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x01,0x00,0xa0,0xde,
|
||||
0x97,0x6c,0xd1,0x11,0x82,0x71,0x00,0xa0,0x24,0x42,0xdf,0x7d,0x01,0x00,0x00,0x00,
|
||||
0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00)
|
||||
|
||||
|
||||
-- The Rules
|
||||
portrule = shortport.port_or_service(34964, "profinet-cm", "udp")
|
||||
if not nmap.is_privileged() then
|
||||
stdnse.debug(1, "Nmap is NOT running as privileged.")
|
||||
portrule = nil
|
||||
prerule = function() return false end
|
||||
end
|
||||
|
||||
-- The Action
|
||||
|
||||
---
|
||||
-- Parses the EPM Lookup Response extracting the annotation field containing the
|
||||
-- product name and the article number of the scanned PNIO Device
|
||||
---
|
||||
parse_response = function(host, port, layer3)
|
||||
-- print raw bytes of reponse
|
||||
stdnse.debug(2, "Raw hex: %s", stdnse.tohex(layer3))
|
||||
|
||||
-- parse byte order/ endianness
|
||||
local order_tmp = string.unpack('B', layer3, 33)
|
||||
local order = order_tmp >> 4
|
||||
local format_prefix = order == 0 and ">" or "<"
|
||||
|
||||
stdnse.debug(1, "little_endian: " .. tostring(order))
|
||||
|
||||
-- parse annotationOffset
|
||||
local annotationOffset = string.unpack("I4", layer3, 165)
|
||||
stdnse.debug(1, "annotationOffset 0x%s", stdnse.tohex(annotationOffset))
|
||||
|
||||
-- parse annotationLength
|
||||
local annotation_length_format = string.format("%si4", format_prefix)
|
||||
stdnse.debug(1, annotation_length_format)
|
||||
local annotationLength = string.unpack(annotation_length_format, layer3, 169)
|
||||
stdnse.debug(1, "annotationLength " .. annotationLength)
|
||||
|
||||
-- parse annotation
|
||||
local annotation_format = string.format("c%d", annotationLength)
|
||||
local annotation = string.unpack(annotation_format, layer3, 173)
|
||||
stdnse.debug(1, "annotation: " .. annotation)
|
||||
|
||||
-- create table for output
|
||||
local output = stdnse.output_table()
|
||||
output["ipAddress"] = host.ip
|
||||
output["annotationOffset"] = annotationOffset
|
||||
output["annotationLength"] = annotationLength
|
||||
output["annotation"] = annotation
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
-- Sends the udp payload and parses the response
|
||||
lookup_request = function(host, port, payload, timeout)
|
||||
local socket, try, catch
|
||||
|
||||
-- create a new udp socket for sending the lookup request
|
||||
local socket = nmap.new_socket("udp")
|
||||
|
||||
-- create a socket for receiving incoming data
|
||||
-- 'socket:receive()'' alone won't suffice as the UDP port of
|
||||
-- the scanned device can be selected arbitrarily
|
||||
local pcap = nmap.new_socket()
|
||||
|
||||
-- set timeout
|
||||
socket:set_timeout(tonumber(timeout))
|
||||
|
||||
catch = function()
|
||||
pcap:close()
|
||||
socket:close()
|
||||
end
|
||||
|
||||
-- create new try
|
||||
try = nmap.new_try(catch)
|
||||
|
||||
-- connect to port on host for sending payload
|
||||
try(socket:connect(host.ip, port["number"], "udp"))
|
||||
|
||||
local status, lhost, lport, rhost, rport = socket:get_info()
|
||||
|
||||
if status then
|
||||
-- configuration for pcap:pcap_receive()
|
||||
pcap:pcap_open(host.interface, 1500, false, "udp dst port " .. lport .. " and src host " .. host.ip)
|
||||
pcap:set_timeout(host.times.timeout * 1000)
|
||||
|
||||
-- send lookup packet with PNIO Interface UUID
|
||||
try(socket:send(payload))
|
||||
|
||||
-- receive response
|
||||
local status_rec, len, _, layer3 = pcap:pcap_receive()
|
||||
|
||||
-- when successful, set port state to "open" and parse response
|
||||
if status_rec and len > 200 then
|
||||
nmap.set_port_state(host, port, "open")
|
||||
return parse_response(host, port, layer3)
|
||||
end
|
||||
end
|
||||
|
||||
-- close sockets
|
||||
pcap:close()
|
||||
socket:close()
|
||||
|
||||
end
|
||||
|
||||
-- MAIN
|
||||
action = function(host, port)
|
||||
local payload = DCE_RPC_REQUEST .. EPM_Lookup
|
||||
local timeout = stdnse.get_timeout(host)
|
||||
return lookup_request(host, port, payload, timeout)
|
||||
end
|
||||
@@ -145,6 +145,7 @@ Entry { filename = "hadoop-jobtracker-info.nse", categories = { "default", "disc
|
||||
Entry { filename = "hadoop-namenode-info.nse", categories = { "default", "discovery", "safe", } }
|
||||
Entry { filename = "hadoop-secondary-namenode-info.nse", categories = { "default", "discovery", "safe", } }
|
||||
Entry { filename = "hadoop-tasktracker-info.nse", categories = { "default", "discovery", "safe", } }
|
||||
Entry { filename = "hartip-info.nse", categories = { "discovery", "intrusive", } }
|
||||
Entry { filename = "hbase-master-info.nse", categories = { "default", "discovery", "safe", } }
|
||||
Entry { filename = "hbase-region-info.nse", categories = { "default", "discovery", "safe", } }
|
||||
Entry { filename = "hddtemp-info.nse", categories = { "default", "discovery", "safe", } }
|
||||
@@ -291,6 +292,7 @@ Entry { filename = "iax2-brute.nse", categories = { "brute", "intrusive", } }
|
||||
Entry { filename = "iax2-version.nse", categories = { "version", } }
|
||||
Entry { filename = "icap-info.nse", categories = { "discovery", "safe", } }
|
||||
Entry { filename = "iec-identify.nse", categories = { "discovery", "intrusive", } }
|
||||
Entry { filename = "iec61850-mms.nse", categories = { "discovery", "intrusive", "version", } }
|
||||
Entry { filename = "ike-version.nse", categories = { "default", "discovery", "safe", "version", } }
|
||||
Entry { filename = "imap-brute.nse", categories = { "brute", "intrusive", } }
|
||||
Entry { filename = "imap-capabilities.nse", categories = { "default", "safe", } }
|
||||
@@ -367,6 +369,7 @@ Entry { filename = "ms-sql-tables.nse", categories = { "discovery", "safe", } }
|
||||
Entry { filename = "ms-sql-xp-cmdshell.nse", categories = { "intrusive", } }
|
||||
Entry { filename = "msrpc-enum.nse", categories = { "discovery", "safe", } }
|
||||
Entry { filename = "mtrace.nse", categories = { "broadcast", "discovery", "safe", } }
|
||||
Entry { filename = "multicast-profinet-discovery.nse", categories = { "discovery", "info", "safe", } }
|
||||
Entry { filename = "murmur-version.nse", categories = { "version", } }
|
||||
Entry { filename = "mysql-audit.nse", categories = { "discovery", "safe", } }
|
||||
Entry { filename = "mysql-brute.nse", categories = { "brute", "intrusive", } }
|
||||
@@ -429,6 +432,7 @@ Entry { filename = "pop3-capabilities.nse", categories = { "default", "discovery
|
||||
Entry { filename = "pop3-ntlm-info.nse", categories = { "default", "discovery", "safe", } }
|
||||
Entry { filename = "port-states.nse", categories = { "safe", } }
|
||||
Entry { filename = "pptp-version.nse", categories = { "version", } }
|
||||
Entry { filename = "profinet-cm-lookup.nse", categories = { "discovery", "intrusive", } }
|
||||
Entry { filename = "puppet-naivesigning.nse", categories = { "intrusive", "vuln", } }
|
||||
Entry { filename = "qconn-exec.nse", categories = { "exploit", "intrusive", "vuln", } }
|
||||
Entry { filename = "qscan.nse", categories = { "discovery", "safe", } }
|
||||
|
||||
Reference in New Issue
Block a user