From c4a4e0db45ecd5594d131f8316efe7e18df5ab6b Mon Sep 17 00:00:00 2001 From: dmiller Date: Thu, 11 Apr 2024 16:02:48 +0000 Subject: [PATCH] Add 4 scripts from the DINA Community --- CHANGELOG | 15 + nmap-service-probes | 2 +- nselib/iec61850mms.lua | 1186 ++++++++++++++++++++++ scripts/hartip-info.nse | 304 ++++++ scripts/iec61850-mms.nse | 380 +++++++ scripts/multicast-profinet-discovery.nse | 439 ++++++++ scripts/profinet-cm-lookup.nse | 166 +++ scripts/script.db | 4 + 8 files changed, 2495 insertions(+), 1 deletion(-) create mode 100755 nselib/iec61850mms.lua create mode 100755 scripts/hartip-info.nse create mode 100755 scripts/iec61850-mms.nse create mode 100755 scripts/multicast-profinet-discovery.nse create mode 100755 scripts/profinet-cm-lookup.nse diff --git a/CHANGELOG b/CHANGELOG index e7d90b068..714af83cf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,20 @@ #Nmap Changelog ($Id$); -*-text-*- +o [NSE] Four new scripts from the DINA community (https://github.com/DINA-community) + for querying industrial control systems: + + + hartip-info reads device information from devices using the Highway + Addressable Remote Transducer protocol + + + iec61850-mms queries devices using Manufacturing Message Specification + requests. [Dennis Rösch, Max Helbig] + + + multicast-profinet-discovery Sends a multicast PROFINET DCP Identify All + message and prints the responses. [Stefan Eiwanger, DINA-community] + + + profinet-cm-lookup queries the DCERPC endpoint mapper exposed via the + PNIO-CM service. + o Integrated over 2500 service/version detection fingerprints submitted since June 2020. The signature count went up 1.4% to 12089, including 9 new softmatches. We now detect 1246 protocols, including new additions of grpc, diff --git a/nmap-service-probes b/nmap-service-probes index a3b8b47a1..0056fd434 100644 --- a/nmap-service-probes +++ b/nmap-service-probes @@ -16931,7 +16931,7 @@ rarity 8 ports 123 Probe UDP DCERPC_CALL q|\x05\0\x0b\x03\x10\0\0\0\x48\0\0\0\x01\0\0\0\xb8\x10\xb8\x10\0\0\0\0\x01\0\0\0\0\0\x01\0\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef\xe7\x03\0\0\xfe\xdc\xba\x98\x76\x54\x32\x10\x01\x23\x45\x67\x89\xab\xcd\xef\xe7\x03\0\0| rarity 8 -ports 135,1025-1199 +ports 135,1025-1199,34964 Probe UDP CIFS_NS_UC q|\x01\x91\0\0\0\x01\0\0\0\0\0\0\x20CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\0\0\x21\0\x01| rarity 8 ports 137 diff --git a/nselib/iec61850mms.lua b/nselib/iec61850mms.lua new file mode 100755 index 000000000..3bb92e023 --- /dev/null +++ b/nselib/iec61850mms.lua @@ -0,0 +1,1186 @@ +---Implements decoders and encoders for IEC-61850-8-1 MMS queries +-- +-- References: +-- * https://en.wikipedia.org/wiki/IEC_61850 +-- * https://datatracker.ietf.org/doc/html/rfc1006 +-- +-- @author Dennis Rösch +-- @author Max Helbig +-- @license Same as Nmap--See https://nmap.org/book/man-legal.html +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" +local asn1 = require "asn1" +local math = require "math" + +_ENV = stdnse.module("iec61850mms", stdnse.seeall) + +local function stringToHex(str) + return "\\x" .. stdnse.tohex(str, {separator = "\\x"}) +end + +MMSDecoder = { + + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, + + unpackmmsFromTPKT = function(self, tpktStr) + -- unpack TPKT and COTP + local TPKT_pos = 1 + local COTP_pos = 5 + local COTP_last = false + local TPKT_ver, TPKT_res, TPKT_len + local COTP_len, COTP_type, COTP_tpdu + local OSI_Session = {} + + while not COTP_last do + TPKT_ver, TPKT_res, TPKT_len = string.unpack("i1c1>i2", tpktStr, TPKT_pos) + COTP_len, COTP_type, COTP_tpdu = string.unpack("i1c1c1", tpktStr, COTP_pos) + COTP_last = COTP_tpdu == "\x80" + + OSI_Session[#OSI_Session+1] = string.sub(tpktStr, TPKT_pos + 7, TPKT_pos + TPKT_len - 1) + + + if not COTP_last then + TPKT_pos = TPKT_pos + TPKT_len + COTP_pos = TPKT_pos + 4 + end + end + OSI_Session = table.concat(OSI_Session) + + + local newpos = 5 -- start of ISO 8823 + local type, len, dummy + + -- ISO 8823 OSI + type, newpos = string.unpack("c1", OSI_Session, newpos) + if type ~= "\x61" then + stdnse.debug(1,"not ISO 8823 OSI type is %s: ", stringToHex(type)) + return nil + end + len, newpos = self.decodeLength(OSI_Session, newpos) + + -- presentation-context-identifier + type, newpos = string.unpack("c1", OSI_Session, newpos) + if type ~= "\x30" then + stdnse.debug(1,"not presentation-context-identifier type is %s: ", stringToHex(type)) + return nil + end + len, newpos = self.decodeLength(OSI_Session, newpos) + + -- fully-encoded-data + type, newpos = string.unpack("c1", OSI_Session, newpos) + if type ~= "\x02" then + stdnse.debug(1,"not fully-encoded-data type is %s: ", stringToHex(type)) + return nil + end + len, newpos = self.decodeLength(OSI_Session, newpos) + dummy, newpos = self.decodeInt(OSI_Session, len, newpos) + + -- single-ASN1-type + type, newpos = string.unpack("c1", OSI_Session, newpos) + if type ~= "\xa0" then + stdnse.debug(1,"not single-ASN1-type type is %s: ", stringToHex(type)) + return nil + end + len, newpos = self.decodeLength(OSI_Session, newpos) + + + + return string.sub(OSI_Session, newpos) + end, + + unpackAndDecode = function(self, tpktStr) + local mmsStr = self.unpackmmsFromTPKT(self, tpktStr) + if not mmsStr then + stdnse.debug(1, "mmsString is nil") + return nil + end + return(self.mmsPDU(self, mmsStr)) + end, + + mmsPDU = function(self, mmsStr) + local CHOICE = { + ["\xa0"] = "confirmed_RequestPDU", + ["\xa1"] = "confirmed_ResponsePDU", + ["\xa8"] = "initiate_RequestPDU", + } + + local PDUType, PDUlen + local newpos = 1 + + PDUType, newpos = string.unpack("c1", mmsStr, newpos) + PDUlen, newpos = self.decodeLength(mmsStr, newpos) + + local retval + if CHOICE[PDUType] then + retval = self[CHOICE[PDUType]](self, mmsStr, PDUlen, newpos) + else + stdnse.debug(1,"mmsPDU: no option for type %s", stringToHex(PDUType)) + retval, newpos = self.unknown(self, mmsStr, PDUlen, newpos) + return retval + end + + return {[CHOICE[PDUType]] = retval} + end, + + confirmed_RequestPDU = function(self, str, elen, pos) + local type, len + local newpos = pos + + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + -- invokeID + if type ~= "\x02" then + stdnse.debug(1,"no invokeID in RequestPDU") + return nil + end + + local invokeID + invokeID, newpos = self.decodeInt(str, len, newpos) + + -- service + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + local CHOICE = { + ["\xa4"] = "Read_Request" + } + + local confirmedServiceRequest + if CHOICE[type] then + confirmedServiceRequest = self[CHOICE[type]](self, str, len, newpos) + else + stdnse.debug(1,"unknown confirmedServiceRequest") + confirmedServiceRequest = nil + end + + -- bulid return value + local tab = { + ["invokeID"] = invokeID, + [CHOICE[type]] = confirmedServiceRequest, + } + + local retpos = pos + elen + return tab, retpos + end, + + confirmed_ResponsePDU = function(self, str, elen, pos) + local type, len + local newpos = pos + + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + -- invokeID + if type ~= "\x02" then + stdnse.debug(1,"no invokeID") + return nil + end + + local invokeID + invokeID, newpos = self.decodeInt(str, len, newpos) + + -- service + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + local CHOICE = { + ["\xa1"] = "getNameList", + ["\xa2"] = "identify", + ["\xa4"] = "Read_Response", + } + + local confirmedServiceResponse + if CHOICE[type] then + confirmedServiceResponse = self[CHOICE[type]](self, str, len, newpos) + else + stdnse.debug(1,"unknown confirmedServiceResponse") + confirmedServiceResponse = nil + end + + -- bulid return value + local tab = { + ["invokeID"] = invokeID, + [CHOICE[type]] = confirmedServiceResponse, + } + + local retpos = pos + elen + return tab, pos + elen + end, + + identify = function(self, str, elen, pos) + local CHOICE = { + ["\x80"] = "vendorName", + ["\x81"] = "modelName", + ["\x82"] = "revision", + } + + local seq = {} + local sPos = 1 + local sNum = 0 + local sValue + local newpos = pos + + while (newpos < pos + elen) do + local type, len + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + sValue, newpos = self.decodeStr( str, len, newpos) + sNum = sNum + 1 + seq[CHOICE[type]] = sValue + end + + return seq, pos + elen + end, + + getNameList = function(self, str, elen, pos) + local type, len + local newpos = pos + + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + -- listofidentifier + if type ~= "\xa0" then + stdnse.debug(1,"no list of identifier") + return nil + end + + local idvlist + idvlist, newpos = self.listOfIdentifier(self, str, len, newpos) + local tab = { + ["listOfIdentifier"] = idvlist + } + + if pos+elen-newpos == 3 then + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + local morefollows + morefollows, newpos = self.decodeBool(str, len, newpos) + tab["moreFollows"] = morefollows + else + tab["moreFollows"] = true + end + + return tab, pos + elen + end, + + listOfIdentifier = function(self, str, elen, pos) + local seq = {} + local sPos = 1 + local sNum = 0 + local sValue + local newpos = pos + + while (newpos < pos + elen) do + local type, len + type, newpos = string.unpack("c1", str, newpos) + if type ~= "\x1a" then + stdnse.debug(1,"no identifier type") + end + + len, newpos = self.decodeLength(str, newpos) + sValue, newpos = self.decodeStr( str, len, newpos) + sNum = sNum + 1 + table.insert(seq, sValue) + + end + + return seq, pos + elen + end, + + initiate_RequestPDU = function(self, str, elen, pos) + local seq = {} + local sPos = 1 + local sNum = 0 + local sValue + local newpos = pos + local CHOICE = {} -- Submitted with no values. + + while (newpos < pos + elen) do + local type, len + type, newpos = string.unpack("c1", str, newpos) + if CHOICE[type] == nil then + stdnse.debug(1,"no type for %s", stringToHex(type)) + end + len, newpos = self.decodeLength(str, newpos) + sValue, newpos = self[CHOICE[type]](self, str, len, newpos) + sNum = sNum + 1 + seq[CHOICE[type]] = sValue + end + + return seq, pos + elen + end, + + localDetailCalling = function(self, str, elen, pos) + return self.integer(self, str, elen, pos) + end, + + proposedMaxServOutstandingCalling = function(self, str, elen, pos) + return self.integer(self, str, elen, pos) + end, + + proposedMaxServOutstandingCalled = function(self, str, elen, pos) + return self.integer(self, str, elen, pos) + end, + + proposedDataStructureNestingLevel = function(self, str, elen, pos) + return self.integer(self, str, elen, pos) + end, + + initRequestDetail = function(self, str, elen, pos) + local CHOICE = { + ["\x80"] = "proposedVersionNumber", + ["\x81"] = "parameterSupportOptions", + ["\x82"] = "servicesSupportedCalling", + } + + local seq = {} + local sPos = 1 + local sNum = 0 + local sValue + local newpos = pos + + while (newpos < pos + elen) do + local type, len + type, newpos = string.unpack("c1", str, newpos) + if CHOICE[type] == nil then + stdnse.debug(1,"no type for %s", stringToHex(type)) + end + len, newpos = self.decodeLength(str, newpos) + sValue, newpos = self[CHOICE[type]](self, str, len, newpos) + sNum = sNum + 1 + seq[CHOICE[type]] = sValue + end + + return seq, pos + elen + end, + + parameterSupportOptions = function(self, str, elen, pos) + local NAMES = { + "array support", + "structure support", + "named variable support", + "structure support", + "alternate access support", + "unnamed variable support", + "scattered access support", + "third party operations support", + "named variable list support", + "condition event support" + } + + return self.bit_string(self, str, elen, pos, NAMES) + end, + + proposedVersionNumber = function(self, str, elen, pos) + return self.integer(self, str, elen, pos) + end, + + Read_Response = function(self, str, elen, pos) + local type, len + local newpos = pos + + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + -- listOfAccessResult + local listOfAccessResult + if type ~= "\xa1" then + stdnse.debug(1,"no listOfAccessResult") + return nil, pos + elen + end + + listOfAccessResult, newpos = self.listOfAccessResult(self, str, len, newpos) + + -- bulid return value + local tab = { + ["listOfAccessResult"] = listOfAccessResult + } + return tab, pos + elen + end, + + Read_Request = function(self, str, elen, pos) + local type, len + local newpos = pos + + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + local specificationWithResult + if type ~= "\x80" then + stdnse.debug(1,"no specificationWithResult") + specificationWithResult = nil + end + specificationWithResult, newpos = self.decodeBool(str, len, newpos) + + -- variableAccessSpecification + local variableAccessSpecification + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + if type ~= "\xa1" then + stdnse.debug(1,"no variableAccessSpecification") + return nil, pos + elen + end + + variableAccessSpecification, newpos = self.variableAccessSpecification(self, str, len, newpos) + + -- bulid return value + local tab = { + ["specificationWithResult"] = specificationWithResult, + ["variableAccessSpecification"] = variableAccessSpecification, + } + + local retpos = pos + elen + return tab, retpos + end, + + listOfAccessResult = function(self, str, elen, pos) + local newpos = pos + + local seq = {} + local sPos = 1 + local sNum = 0 + local sValue + + while (newpos < pos + elen) do + local type, len + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + sValue, newpos = self.accessResult(self, str, len, newpos, type) + sNum = sNum + 1 + table.insert(seq, sValue) + end + + return seq, pos + elen + end, + + accessResult = function(self, str, elen, pos, type) + local CHOICE = { + ["\xa2"] = "structure", + ["\x80"] = "dataAccessError", + ["\x83"] = "bool", + ["\x84"] = "bit_string", + ["\x85"] = "integer", + ["\x86"] = "unsigned", + ["\x89"] = "octet_string", + ["\x8a"] = "string", + ["\x8c"] = "binaryTime", + ["\x91"] = "utc_Time", + } + + local seq = {} + local sPos = 1 + local sNum = 0 + local sValue + local newpos = pos + + if elen == 0 and CHOICE[type] == "string" then + table.insert(seq, "") + end + + while (newpos < pos + elen) do + if CHOICE[type] == nil then + stdnse.debug(1,"no type for", stringToHex(type)) + end + sValue, newpos = self[CHOICE[type]](self, str, elen, newpos) + sNum = sNum + 1 + table.insert(seq, sValue) + end + + return seq, pos + elen + end, + + dataAccessError = function(self, str, elen, pos) + local CHOICE = { + ["\x00"] = "object-invalidated", + ["\x01"] = "hardware-fault", + ["\x02"] = "temporarily-unavalible", + ["\x03"] = "object-access-denied", + ["\x04"] = "object-undefined", + ["\x05"] = "invalid-address", + ["\x06"] = "type-unsupported", + ["\x07"] = "type-inconsistent", + ["\x08"] = "object-attribute-inconsistent", + ["\x09"] = "object-access-unsupported", + ["\x0a"] = "object-non-existent", + ["\x0b"] = "object-value-invalid", + } + + local num, newpos = string.unpack("c" .. elen, str, pos) + local retval = "DataAccessError: " .. CHOICE[num] + return retval, pos + elen + end, + + structure = function(self, str, elen, pos) + local CHOICE = { + ["\xa2"] = "structure", + ["\x83"] = "bool", + ["\x84"] = "bit_string", + ["\x85"] = "integer", + ["\x86"] = "unsigned", + ["\x89"] = "octet_string", + ["\x8a"] = "string", + ["\x8c"] = "binaryTime", + ["\x91"] = "utc_Time", + } + + local seq = {} + local sPos = 1 + local sNum = 0 + local sValue + local newpos = pos + + while (newpos < pos + elen) do + local type, len + type, newpos = string.unpack("c1", str, newpos) + if CHOICE[type] == nil then + stdnse.debug(1,"no type for", stringToHex(type)) + end + len, newpos = self.decodeLength(str, newpos) + sValue, newpos = self[CHOICE[type]](self, str, len, newpos) + sNum = sNum + 1 + table.insert(seq, sValue) + end + + return seq, pos + elen + end, + + bool = function(self, str, elen, pos) + return "TODO: bool", pos + elen + end, + + bit_string = function(self, str, elen, pos, names) + local padding, newpos = self.decodeInt(str, 1, pos) + + return "TODO: bit_string", pos + elen + end, + + integer = function(self, str, elen, pos) + return self.decodeInt(str, elen, pos) + end, + + unsigned = function(self, str, elen, pos) + return "TODO: unsigned", pos + elen + end, + + octet_string = function(self, str, elen, pos) + return "TODO: string", pos + elen + end, + + string = function(self, str, elen, pos) + return string.unpack("c" .. elen, str, pos) + end, + + binaryTime = function(self, str, elen, pos) + return "TODO: string", pos + elen + end, + + utc_Time= function(self, str, elen, pos) + return "TODO: utc_Time", pos + elen + end, + + unknown = function(self, str, elen, pos) + local hex = stringToHex(str) + stdnse.debug(1,"Decoder: got an unknown Type") + stdnse.debug(1,"embedded String in hex:\n", hex) + stdnse.debug(1,"length of string given to coder: ", #str) + stdnse.debug(1,"Current position of coder: ", pos) + + return str + end, + + variableAccessSpecification = function(self, str, elen, pos) + local type, len + local newpos = pos + + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + local listOfVariable + if type ~= "\xa0" then + stdnse.debug(1,"no listOfVariable") + listOfVariable = nil + end + listOfVariable, newpos = self.listOfVariable(self, str, len, newpos) + + local tab = { + ["listOfVariable"] = listOfVariable + } + + return tab, pos + elen + end, + + listOfVariable = function(self, str, elen, pos) + local newpos = pos + + local seq = {} + local sPos = 1 + local sNum = 0 + local sValue + + while (newpos < pos + elen) do + local type, len + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + sValue, newpos = self.variableSpecification(self, str, len, newpos) + sNum = sNum + 1 + table.insert(seq, sValue) + end + + return seq, pos + elen + end, + + variableSpecification = function(self, str, elen, pos) + local type, len + local newpos = pos + + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + local CHOICE = { + ["\xa0"] = "objectName" + } + + + local retval + if CHOICE[type] then + retval = self[CHOICE[type]](self, str, len, newpos) + else + retval = nil + end + + local tab = { + [CHOICE[type]] = retval + } + return tab, pos + elen + end, + + objectName = function(self, str, elen, pos) + local type, len + local newpos = pos + + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + local CHOICE = { + ["\xa1"] = "domain_specific" + } + + local retval + if CHOICE[type] then + retval = self[CHOICE[type]](self, str, len, newpos) + else + retval = nil + end + + local tab = { + [CHOICE[type]] = retval + } + return tab, pos + elen + end, + + domain_specific = function(self, str, elen, pos) + local type, len + local newpos = pos + + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + local domainID, itemID + domainID, newpos = self.decodeStr(str, len, newpos) + + type, newpos = string.unpack("c1", str, newpos) + len, newpos = self.decodeLength(str, newpos) + + itemID, newpos = self.decodeStr(str, len, newpos) + + local tab = { + ["domainID"] = domainID, + ["itemID"] = itemID, + } + + return tab, pos + elen + end, + + decodeLength = asn1.ASN1Decoder.decodeLength, + + decodeInt = asn1.ASN1Decoder.decodeInt, + + decodeBool = function( str, elen, pos ) + local val = string.byte(str, pos) + return val ~= 0, pos + 1 + end, + + decodeStr = function(encStr, elen, pos ) + return string.unpack("c" .. elen, encStr, pos) + end +} + +MMSEncoder = { + + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, + + packmmsInTPKT = function(self, mmsStr) + local sendstr = mmsStr + sendstr = "\xa0"..self.encodeLength(#sendstr)..sendstr + sendstr = "\x02\x01\x03"..sendstr + sendstr = self.encodeSeq(sendstr) + sendstr = "\x61"..self.encodeLength(#sendstr)..sendstr --ISO8823 + sendstr = "\x01\x00\x01\x00"..sendstr --ISO8327 2x + sendstr = "\x02\xf0\x80"..sendstr --ISO8073 + local final_len = #sendstr+4 + sendstr = "\x03\x00"..string.char(math.floor(final_len / 256), final_len % 256)..sendstr + return sendstr + end, + + encodeAndPack = function(self, mmsTab) + local mmsStr = self.mmsPDU(self, mmsTab) + return self.packmmsInTPKT(self, mmsStr) + end, + + mmsPDU = function(self, message) + local CHOICE = { + ["confirmed_RequestPDU"] = "\xa0", + ["confirmed_ResponsePDU"] = "\xa1", + } + + local type = type(message) + + if type ~= 'table' then + stdnse.print_debug(1,"mmsPDU: must be a table") + return nil + end + + if self.tabElementCount(message) ~= 1 then + stdnse.print_debug(1,"mmsPDU: table muss have exactly one element") + return nil + end + + local key, val = next(message) + if not CHOICE[key] then + stdnse.print_debug(1,"mmsPDU: no PDU type ", key) + return nil + end + + local pdustr = self[key](self, message[key]) + local retstr = CHOICE[key].. self.encodeLength(#pdustr) .. pdustr + return retstr + + end, + + confirmed_RequestPDU = function(self, message) + local CHOICE = { + ["Read_Request"] = "\xa4", + ["getNameList"] = "\xa1", + } + + if type(message) ~= 'table' then + stdnse.print_debug(1,"confirmed_RequestPDU: must be a table") + return "" + end + + local tablen = self.tabElementCount(message) + if tablen < 2 or tablen > 4 then + stdnse.print_debug(1,"confirmed_RequestPDU: table must have between 2 and 4 elements") + return "" + end + + if not message["invokeID"] then + stdnse.print_debug(1,"confirmed_RequestPDU: message must contain invokeID ") + return "" + end + + local confServReqKey = self.tabContainsKeyOfTab(message, CHOICE) + if not confServReqKey then + stdnse.print_debug(1,"confirmed_RequestPDU: message must contain confirmedServiceRequest") + return "" + end + + local invokeID = self.encodeInt(message["invokeID"]) + local retstr = "\x02" .. self.encodeLength(#invokeID) .. invokeID + + local confirmedServiceRequest = self[confServReqKey](self, message[confServReqKey]) + retstr = retstr .. CHOICE[confServReqKey] .. self.encodeLength(#confirmedServiceRequest) .. confirmedServiceRequest + + return retstr + end, + + getNameList = function(self, message) + if type(message) ~= 'table' then + stdnse.debug(1,"getNameList: must be a table") + return "" + end + + if message["objectClass"] == nil then + stdnse.debug(1,"getNameList: message must contain objectClass") + return "" + end + + local oC = self.objectClass(self, message["objectClass"]) + local retstr = "\xa0" .. self.encodeLength(#oC) .. oC + + if message["objectScope"] == nil then + stdnse.debug(1,"getNameList: message must contain objectScope") + return "" + end + + local oS = self.objectScope(self, message["objectScope"]) + retstr = retstr .. "\xa1" .. self.encodeLength(#oS) .. oS + + if message["continueAfter"] ~= nil then + local continueAfter = self.encodeStr(message["continueAfter"]) + retstr = retstr .. "\x82" .. self.encodeLength(#continueAfter) .. continueAfter + end + + return retstr + end, + + objectClass = function(self, message) + if type(message) ~= 'string' then + stdnse.debug(1,"objectClass: must be a String") + return "" + end + + local CHOICE = { + ["namedVariable"] = 0, + ["domain"] = 9, + } + + if CHOICE[message] == nil then + stdnse.debug(1,"objectClass: message not valid") + return "" + end + local res = self.encodeInt(CHOICE[message]) + local retstr = "\x80" .. self.encodeLength(#res) .. res + + return retstr + end, + + objectScope = function(self, message) + if type(message) ~= 'table' then + stdnse.debug(1,"objectScope: must be a table") + return "" + end + + local tablen = self.tabElementCount(message) + if tablen ~= 1 then + stdnse.print_debug(1,"objectScope: table must have 1 element") + return "" + end + + local CHOICE = { + ["vmdSpecific"] = "\x80", + ["domainSpecific"] = "\x81", + } + + local Key = self.tabContainsKeyOfTab(message, CHOICE) + if not Key then + stdnse.print_debug(1,"objectScope: message must contain valid element") + return "" + end + + local res = self[Key](self, message[Key]) + local retstr = CHOICE[Key] .. self.encodeLength(#res) .. res + + return retstr + end, + + domainSpecific = function(self, message) + return self.encodeStr(message) + end, + + vmdSpecific = function(self, message) + return "" + end, + + Read_Request = function(self, message) + local type = type(message) + + if type ~= 'table' then + stdnse.print_debug(1,"Read_Request: must be a table") + return "" + end + + local tablen = self.tabElementCount(message) + if tablen ~= 2 then + stdnse.print_debug(1,"Read_Request: table must have 2 elements") + return "" + end + + if message["specificationWithResult"] == nil then + stdnse.print_debug(1,"Read_Request: message must contain specificationWithResult") + return "" + end + + if message["variableAccessSpecification"] == nil then + stdnse.print_debug(1,"Read_Request: message must contain variableAccessSpecification") + return "" + end + + local specificationWithResult = self.encodeBool(message["specificationWithResult"]) + local retstr = "\x80" .. self.encodeLength(#specificationWithResult) .. specificationWithResult + + local variableAccessSpecification = self.variableAccessSpecification(self, message["variableAccessSpecification"] ) + retstr = retstr .. "\xa1" .. self.encodeLength(#variableAccessSpecification) .. variableAccessSpecification + + return retstr + end, + + variableAccessSpecification = function(self, message) + local type = type(message) + + if type ~= 'table' then + stdnse.print_debug(1,"variableAccessSpecification: must be a table") + return "" + end + + local tablen = self.tabElementCount(message) + if tablen ~= 1 then + stdnse.print_debug(1,"variableAccessSpecification: table must have 1 element") + return "" + end + + if message["listOfVariable"] == nil then + stdnse.print_debug(1,"variableAccessSpecification: message must contain listOfVariable") + return "" + end + local listOfVariable = self.listOfVariable(self, message["listOfVariable"]) + local retstr = "\xa0" .. self.encodeLength(#listOfVariable) .. listOfVariable + + return retstr + end, + + listOfVariable = function(self, message) + local type = type(message) + + if type ~= 'table' then + stdnse.print_debug(1,"listOfVariable: must be a table") + return "" + end + + local retstr = {} + local value + for k, v in pairs(message) do + value = self.variableSpecification(self, v) + retstr[#retstr+1] = self.encodeSeq(value) + end + + return table.concat(retstr) + end, + + variableSpecification = function (self, message) + local CHOICE = { + ["objectName"] = "\xa0", + } + + local type = type(message) + + if type ~= 'table' then + stdnse.print_debug(1,"variableSpecification: must be a table") + return "" + end + + local tablen = self.tabElementCount(message) + if tablen ~= 1 then + stdnse.print_debug(1,"variableSpecification: table must have 1 element") + return "" + end + + local varSpec = self.tabContainsKeyOfTab(message, CHOICE) + if not varSpec then + stdnse.print_debug(1,"variableSpecification: message must contain variableSpecification") + return "" + end + + local specstr = self[varSpec](self, message[varSpec]) + local retstr = CHOICE[varSpec] .. self.encodeLength(#specstr) .. specstr + + return retstr + end, + + objectName = function (self, message) + local CHOICE = { + ["vmd_specific"] = "\xa0", + ["domain_specific"] = "\xa1", + ["aa_specific"] = "\xa2", + } + + local type = type(message) + + if type ~= 'table' then + stdnse.print_debug(1,"objectName: must be a table") + return "" + end + + local tablen = self.tabElementCount(message) + if tablen ~= 1 then + stdnse.print_debug(1,"objectName: table must have 1 element") + return "" + end + + local key = self.tabContainsKeyOfTab(message, CHOICE) + if not key then + stdnse.print_debug(1,"objectName: must contain objectName") + return "" + end + + local value = self[key](self, message[key]) + local retstr = CHOICE[key] .. self.encodeLength(#value) .. value + return retstr + end, + + domain_specific = function(self, message) + local type = type(message) + + if type ~= 'table' then + stdnse.print_debug(1,"domain_specific: must be a table") + return "" + end + + local tablen = self.tabElementCount(message) + if tablen ~= 2 then + stdnse.print_debug(1,"objectName: table must have 2 elements") + return "" + end + + if message["domainID"] == nil then + stdnse.print_debug(1,"domain_specific: message must contain domainID") + return "" + end + + if message["itemID"] == nil then + stdnse.print_debug(1,"domain_specific: message must contain itemID") + return "" + end + + local retstr = "" + local valstr + + valstr = self.encodeStr(message["domainID"]) + retstr = retstr .. "\x1a" .. self.encodeLength(#valstr) .. valstr + + valstr = self.encodeStr(message["itemID"]) + retstr = retstr .. "\x1a" .. self.encodeLength(#valstr) .. valstr + + return retstr + end, + + tabContainsKeyOfTab = function(tab, source) + local retval = nil + for key, val in pairs(source) do + if tab[key] then + retval = key + break + end + end + return retval + end, + + tabElementCount = function(tab) + local count = 0 + for _ in pairs(tab) do count = count + 1 end + return count + end, + + encodeLength = asn1.ASN1Encoder.encodeLength, + + encodeInt = asn1.ASN1Encoder.encodeInt, + + encodeBool = function(val) + if val then + return '\xFF' + else + return '\x00' + end + end, + + encodeStr = function(str) + return str + end, + + encodeSeq = asn1.ASN1Encoder.encodeSeq, +} + +MMSQueries = { + new = function(self,o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o + end, + + askfor = function(self, invokeID, domainID, itemIDs) + local iIdType = type(itemIDs) + + -- make table if we got a single value + if type(itemIDs) ~= 'table' then + itemIDs = {itemIDs} + end + + -- check if all elements are strings + for _, value in pairs(itemIDs) do + if type(value) ~= 'string' then + stdnse.print_debug(1,"All itemIDs must be strings!") + return nil + end + end + + --create structure + local tab = {} + for k, v in pairs(itemIDs) do + local objName = {objectName = {domain_specific = {itemID = v, domainID = domainID}}} + table.insert(tab, objName) + end + local rr = { + variableAccessSpecification = {listOfVariable = tab}, + specificationWithResult = false + } + + local structure = {confirmed_RequestPDU = { Read_Request = rr, invokeID = invokeID}} + + -- encode and return + local encoder = MMSEncoder:new() + local result = encoder:mmsPDU(structure) + return result + end, + + nameList = function(self, invokeID, objectScope, continueAfter) + if invokeID == nil then + stdnse.debug(1, "no invokeID setting to 1") + invokeID = 1 + end + + local oC + local oS + if objectScope == nil then + oC = "domain" + oS = {vmdSpecific = ""} + else + oC = "namedVariable" + oS = {domainSpecific = objectScope} + end + local cA = continueAfter + + + local cSR = {objectClass = oC, objectScope = oS} + if cA ~= nil and cA ~= "" then + cSR["continueAfter"] = cA + end + local structure = {confirmed_RequestPDU = { getNameList = cSR, invokeID = invokeID}} + return structure + end +} + +return _ENV; diff --git a/scripts/hartip-info.nse b/scripts/hartip-info.nse new file mode 100755 index 000000000..cf6df54eb --- /dev/null +++ b/scripts/hartip-info.nse @@ -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 -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 +--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 + +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 diff --git a/scripts/iec61850-mms.nse b/scripts/iec61850-mms.nse new file mode 100755 index 000000000..953780b3a --- /dev/null +++ b/scripts/iec61850-mms.nse @@ -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 +-- + +--- +-- @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] = "" + 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"] = "" + output["serialNumber"] = "" + output["modelNumber"] = "" + output["firmwareVersion"] = "" + end + return output + +end diff --git a/scripts/multicast-profinet-discovery.nse b/scripts/multicast-profinet-discovery.nse new file mode 100755 index 000000000..a811a55dd --- /dev/null +++ b/scripts/multicast-profinet-discovery.nse @@ -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 diff --git a/scripts/profinet-cm-lookup.nse b/scripts/profinet-cm-lookup.nse new file mode 100755 index 000000000..72abe4909 --- /dev/null +++ b/scripts/profinet-cm-lookup.nse @@ -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 -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 +--192.168.10.12 +--0 +--64 +--S7-1500 6ES7 672-5DC01-0YA0 0 V 2 1 7 + +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 diff --git a/scripts/script.db b/scripts/script.db index df9899f11..b4b03cfee 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -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", } }