1
0
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:
dmiller
2024-04-11 16:02:48 +00:00
parent db9a5801d0
commit c4a4e0db45
8 changed files with 2495 additions and 1 deletions

304
scripts/hartip-info.nse Executable file
View 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
View 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

View 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
View 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

View File

@@ -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", } }